Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 */
1441 ariadna 26
final class api_test extends \advanced_testcase {
1 efrain 27
    /**
28
     * Data provider for has_additional_results.
29
     *
30
     * @return array
31
     */
1441 ariadna 32
    public static function has_additional_results_provider(): array {
1 efrain 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
     */
11 efrain 77
    public function test_has_additional_results($result, $expected): void {
1 efrain 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
     */
1441 ariadna 91
    public static function check_and_handle_api_errors_provider(): array {
1 efrain 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
     */
11 efrain 153
    public function test_check_and_handle_api_errors($info, $data, $exception, $exceptionmessage): void {
1 efrain 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
 
164
        if ($exception) {
165
            $this->expectException($exception);
166
        }
167
 
168
        if ($exceptionmessage) {
169
            $this->expectExceptionMessage($exceptionmessage);
170
        }
171
 
172
        $result = $rcm->invoke($mock, $data);
173
 
174
        $this->assertNull($result);
175
    }
176
 
177
    /**
178
     * Data provider for the supports_thumbnail function.
179
     *
180
     * @return array
181
     */
1441 ariadna 182
    public static function supports_thumbnail_provider(): array {
1 efrain 183
        $tests = [
184
            'Only files support thumbnails' => [
185
                (object) ['.tag' => 'folder'],
186
                false,
187
            ],
188
            'Dropbox currently only supports thumbnail generation for files under 20MB' => [
189
                (object) [
190
                    '.tag'          => 'file',
191
                    'size'          => 21 * 1024 * 1024,
192
                ],
193
                false,
194
            ],
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',
200
                ],
201
                false,
202
            ],
203
            'Unusual file extension ending in a working extension' => [
204
                (object) [
205
                    '.tag'          => 'file',
206
                    'size'          => 100 * 1024,
207
                    'path_lower'    => 'Example.pdf.jpg',
208
                ],
209
                true,
210
            ],
211
        ];
212
 
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,
225
            ];
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}",
232
                ],
233
                $result,
234
            ];
235
        }
236
 
237
        return $tests;
238
    }
239
 
240
    /**
241
     * Test the supports_thumbnail function.
242
     *
243
     * @dataProvider supports_thumbnail_provider
244
     * @param   object      $entry      The entry to test
245
     * @param   bool        $expected   Whether this entry supports thumbnail generation
246
     */
11 efrain 247
    public function test_supports_thumbnail($entry, $expected): void {
1 efrain 248
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
249
            ->disableOriginalConstructor()
250
            ->onlyMethods([])
251
            ->getMock();
252
 
253
        $this->assertEquals($expected, $mock->supports_thumbnail($entry));
254
    }
255
 
256
    /**
257
     * Test that the logout makes a call to the correct revocation endpoint.
258
     */
11 efrain 259
    public function test_logout_revocation(): void {
1 efrain 260
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
261
            ->disableOriginalConstructor()
262
            ->onlyMethods(['fetch_dropbox_data'])
263
            ->getMock();
264
 
265
        $mock->expects($this->once())
266
            ->method('fetch_dropbox_data')
267
            ->with($this->equalTo('auth/token/revoke'), $this->equalTo(null));
268
 
269
        $this->assertNull($mock->logout());
270
    }
271
 
272
    /**
273
     * Test that the logout function catches authentication_exception exceptions and discards them.
274
     */
11 efrain 275
    public function test_logout_revocation_catch_auth_exception(): void {
1 efrain 276
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
277
            ->disableOriginalConstructor()
278
            ->onlyMethods(['fetch_dropbox_data'])
279
            ->getMock();
280
 
281
        $mock->expects($this->once())
282
            ->method('fetch_dropbox_data')
283
            ->will($this->throwException(new \repository_dropbox\authentication_exception('Exception should be caught')));
284
 
285
        $this->assertNull($mock->logout());
286
    }
287
 
288
    /**
289
     * Test that the logout function does not catch any other exception.
290
     */
11 efrain 291
    public function test_logout_revocation_does_not_catch_other_exceptions(): void {
1 efrain 292
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
293
            ->disableOriginalConstructor()
294
            ->onlyMethods(['fetch_dropbox_data'])
295
            ->getMock();
296
 
297
        $mock->expects($this->once())
298
            ->method('fetch_dropbox_data')
299
            ->will($this->throwException(new \repository_dropbox\rate_limit_exception));
300
 
301
        $this->expectException(\repository_dropbox\rate_limit_exception::class);
302
        $mock->logout();
303
    }
304
 
305
    /**
306
     * Test basic fetch_dropbox_data function.
307
     */
11 efrain 308
    public function test_fetch_dropbox_data_endpoint(): void {
1 efrain 309
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
310
            ->disableOriginalConstructor()
311
            ->onlyMethods([
312
                'request',
313
                'get_api_endpoint',
314
                'get_content_endpoint',
315
            ])
316
            ->getMock();
317
 
318
        $endpoint = 'testEndpoint';
319
 
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}"));
325
 
326
        $mock->expects($this->never())
327
            ->method('get_content_endpoint');
328
 
329
        $mock->expects($this->once())
330
            ->method('request')
331
            ->will($this->returnValue(json_encode([])));
332
 
333
        // Make the call.
334
        $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
335
        $rcm = $rc->getMethod('fetch_dropbox_data');
336
        $rcm->invoke($mock, $endpoint);
337
    }
338
 
339
    /**
340
     * Some Dropbox endpoints require that the POSTFIELDS be set to null exactly.
341
     */
11 efrain 342
    public function test_fetch_dropbox_data_postfields_null(): void {
1 efrain 343
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
344
            ->disableOriginalConstructor()
345
            ->onlyMethods([
346
                'request',
347
            ])
348
            ->getMock();
349
 
350
        $endpoint = 'testEndpoint';
351
 
352
        $mock->expects($this->once())
353
            ->method('request')
354
            ->with($this->anything(), $this->callback(function($d) {
355
                    return $d['CURLOPT_POSTFIELDS'] === 'null';
356
                }))
357
            ->will($this->returnValue(json_encode([])));
358
 
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);
363
    }
364
 
365
    /**
366
     * When data is specified, it should be json_encoded in POSTFIELDS.
367
     */
11 efrain 368
    public function test_fetch_dropbox_data_postfields_data(): void {
1 efrain 369
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
370
            ->disableOriginalConstructor()
371
            ->onlyMethods([
372
                'request',
373
            ])
374
            ->getMock();
375
 
376
        $endpoint = 'testEndpoint';
377
        $data = ['something' => 'somevalue'];
378
 
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);
383
                }))
384
            ->will($this->returnValue(json_encode([])));
385
 
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);
390
    }
391
 
392
    /**
393
     * When more results are available, these should be fetched until there are no more.
394
     */
11 efrain 395
    public function test_fetch_dropbox_data_recurse_on_additional_records(): void {
1 efrain 396
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
397
            ->disableOriginalConstructor()
398
            ->onlyMethods([
399
                'request',
400
                'get_api_endpoint',
401
            ])
402
            ->getMock();
403
 
404
        $endpoint = 'testEndpoint';
405
 
1441 ariadna 406
        $requestinvocations = $this->exactly(3);
407
        $mock->expects($requestinvocations)
1 efrain 408
            ->method('request')
1441 ariadna 409
            ->willReturnCallback(function () use ($requestinvocations): string {
410
                return match (self::getInvocationCount($requestinvocations)) {
411
                    1 => json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['foo', 'bar']]),
412
                    2 => json_encode(['has_more' => true, 'cursor' => 'Example', 'matches' => ['baz']]),
413
                    3 => json_encode(['has_more' => false, 'cursor' => '', 'matches' => ['bum']]),
414
                    default => $this->fail('Unexpected call to the call() method.'),
415
                };
416
            });
1 efrain 417
 
418
        // We automatically adjust for the /continue endpoint.
1441 ariadna 419
        $apiinvocations = $this->exactly(3);
420
        $mock->expects($apiinvocations)
1 efrain 421
            ->method('get_api_endpoint')
1441 ariadna 422
            ->willReturnCallback(function ($endpoint) use ($apiinvocations): string {
423
                switch (self::getInvocationCount($apiinvocations)) {
424
                    case 1:
425
                        $this->assertEquals('testEndpoint', $endpoint);
426
                        return 'https://example.com/api/2/testEndpoint';
427
                    case 2:
428
                        $this->assertEquals('testEndpoint/continue', $endpoint);
429
                        return 'https://example.com/api/2/testEndpoint/continue';
430
                    case 3:
431
                        $this->assertEquals('testEndpoint/continue', $endpoint);
432
                        return 'https://example.com/api/2/testEndpoint/continue';
433
                    default:
434
                        $this->fail('Unexpected call to the get_api_endpoint() method.');
435
                }
436
            });
1 efrain 437
 
438
        // Make the call.
439
        $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
440
        $rcm = $rc->getMethod('fetch_dropbox_data');
441
        $result = $rcm->invoke($mock, $endpoint, null, 'matches');
442
 
443
        $this->assertEquals([
444
            'foo',
445
            'bar',
446
            'baz',
447
            'bum',
448
        ], $result->matches);
449
 
450
        $this->assertFalse(isset($result->cursor));
451
        $this->assertFalse(isset($result->has_more));
452
    }
453
 
454
    /**
455
     * Base tests for the fetch_dropbox_content function.
456
     */
11 efrain 457
    public function test_fetch_dropbox_content(): void {
1 efrain 458
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
459
            ->disableOriginalConstructor()
460
            ->onlyMethods([
461
                'request',
462
                'setHeader',
463
                'get_content_endpoint',
464
                'get_api_endpoint',
465
                'check_and_handle_api_errors',
466
            ])
467
            ->getMock();
468
 
469
        $data = ['exampledata' => 'examplevalue'];
470
        $endpoint = 'getContent';
471
        $url = "https://example.com/api/2/{$endpoint}";
472
        $response = 'Example content';
473
 
474
        // Only the content endpoint should be called.
475
        $mock->expects($this->once())
476
            ->method('get_content_endpoint')
477
            ->with($endpoint)
478
            ->will($this->returnValue($url));
479
 
480
        $mock->expects($this->never())
481
            ->method('get_api_endpoint');
482
 
1441 ariadna 483
        $headerinvocations = $this->exactly(2);
484
        $mock->expects($headerinvocations)
1 efrain 485
            ->method('setHeader')
1441 ariadna 486
            ->willReturnCallback(function ($header) use ($data, $headerinvocations): void {
487
                switch (self::getInvocationCount($headerinvocations)) {
488
                    case 1:
489
                        $this->assertEquals('Content-Type: ', $header);
490
                        break;
491
                    case 2:
492
                        $this->assertEquals('Dropbox-API-Arg: ' . json_encode($data), $header);
493
                        break;
494
                    default:
495
                        $this->fail('Unexpected call to the setHeader() method.');
496
                }
497
            });
1 efrain 498
 
499
        // Only one request should be made, and it should forcibly be a POST.
500
        $mock->expects($this->once())
501
            ->method('request')
502
            ->with($this->equalTo($url), $this->callback(function($options) {
503
                return $options['CURLOPT_POST'] === 1;
504
            }))
505
            ->willReturn($response);
506
 
507
        $mock->expects($this->once())
508
            ->method('check_and_handle_api_errors')
509
            ->with($this->equalTo($response))
510
            ;
511
 
512
        // Make the call.
513
        $rc = new \ReflectionClass(\repository_dropbox\dropbox::class);
514
        $rcm = $rc->getMethod('fetch_dropbox_content');
515
        $result = $rcm->invoke($mock, $endpoint, $data);
516
 
517
        $this->assertEquals($response, $result);
518
    }
519
 
520
    /**
521
     * Test that the get_file_share_info function returns an existing link if one is available.
522
     */
11 efrain 523
    public function test_get_file_share_info_existing(): void {
1 efrain 524
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
525
            ->disableOriginalConstructor()
526
            ->onlyMethods([
527
                'fetch_dropbox_data',
528
                'normalize_file_share_info',
529
            ])
530
            ->getMock();
531
 
532
        $id = 'LifeTheUniverseAndEverything';
533
        $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
534
        $sharelink = 'https://example.com/share/link';
535
 
536
        // Mock fetch_dropbox_data to return an existing file.
537
        $mock->expects($this->once())
538
            ->method('fetch_dropbox_data')
539
            ->with(
540
                $this->equalTo('sharing/list_shared_links'),
541
                $this->equalTo(['path' => $id])
542
            )
543
            ->willReturn((object) ['links' => [$file]]);
544
 
545
        $mock->expects($this->once())
546
            ->method('normalize_file_share_info')
547
            ->with($this->equalTo($file))
548
            ->will($this->returnValue($sharelink));
549
 
550
        $this->assertEquals($sharelink, $mock->get_file_share_info($id));
551
    }
552
 
553
    /**
554
     * Test that the get_file_share_info function creates a new link if one is not available.
555
     */
11 efrain 556
    public function test_get_file_share_info_new(): void {
1 efrain 557
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
558
            ->disableOriginalConstructor()
559
            ->onlyMethods([
560
                'fetch_dropbox_data',
561
                'normalize_file_share_info',
562
            ])
563
            ->getMock();
564
 
565
        $id = 'LifeTheUniverseAndEverything';
566
        $file = (object) ['.tag' => 'file', 'id' => $id, 'path_lower' => 'SomeValue'];
567
        $sharelink = 'https://example.com/share/link';
568
 
569
        // Mock fetch_dropbox_data to return an existing file.
1441 ariadna 570
        $fetchinvocations = $this->exactly(2);
571
        $mock->expects($fetchinvocations)
1 efrain 572
            ->method('fetch_dropbox_data')
1441 ariadna 573
            ->willReturnCallback(function ($path, $values) use ($fetchinvocations, $id, $file): object {
574
                switch (self::getInvocationCount($fetchinvocations)) {
575
                    case 1:
576
                        $this->assertEquals('sharing/list_shared_links', $path);
577
                        $this->assertEquals(['path' => $id], $values);
578
                        return (object) ['links' => []];
579
                    case 2:
580
                        $this->assertEquals('sharing/create_shared_link_with_settings', $path);
581
                        $this->assertEquals(['path' => $id, 'settings' => ['requested_visibility' => 'public']], $values);
582
                        return $file;
583
                    default:
584
                        $this->fail('Unexpected call to the fetch_dropbox_data() method.');
585
                }
586
            });
1 efrain 587
 
588
        $mock->expects($this->once())
589
            ->method('normalize_file_share_info')
590
            ->with($this->equalTo($file))
591
            ->will($this->returnValue($sharelink));
592
 
593
        $this->assertEquals($sharelink, $mock->get_file_share_info($id));
594
    }
595
 
596
    /**
597
     * Test failure behaviour with get_file_share_info fails to create a new link.
598
     */
11 efrain 599
    public function test_get_file_share_info_new_failure(): void {
1 efrain 600
        $mock = $this->getMockBuilder(\repository_dropbox\dropbox::class)
601
            ->disableOriginalConstructor()
602
            ->onlyMethods([
603
                'fetch_dropbox_data',
604
                'normalize_file_share_info',
605
            ])
606
            ->getMock();
607
 
608
        $id = 'LifeTheUniverseAndEverything';
609
 
610
        // Mock fetch_dropbox_data to return an existing file.
611
        $mock->expects($this->exactly(2))
612
            ->method('fetch_dropbox_data')
1441 ariadna 613
            ->willReturnCallback(function ($path, $values) use ($id): ?object {
614
                switch ($path) {
615
                    case 'sharing/list_shared_links':
616
                        $this->assertEquals(['path' => $id], $values);
617
                        return (object) ['links' => []];
618
                    case 'sharing/create_shared_link_with_settings':
619
                        $this->assertEquals(['path' => $id, 'settings' => ['requested_visibility' => 'public']], $values);
620
                        return null;
621
                    default:
622
                        $this->fail('Unexpected call to the fetch_dropbox_data() method.');
623
                }
624
            });
1 efrain 625
 
626
        $mock->expects($this->never())
627
            ->method('normalize_file_share_info');
628
 
629
        $this->assertNull($mock->get_file_share_info($id));
630
    }
631
}