3 * Copyright 2007 Google Inc.
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 * Google Cloud Storage Stream Wrapper Tests.
20 * CodeSniffer does not handle files with multiple namespaces well.
21 * @codingStandardsIgnoreFile
26 // Mock Memcache class
28 // Mock object to validate calls to memcache
29 static $mock_memcache = null;
31 public static function setMockMemcache($mock) {
32 self
::$mock_memcache = $mock;
34 public function get($keys, $flags = null) {
35 return self
::$mock_memcache->get($keys, $flags);
37 public function set($key, $value, $flag = null, $expire = 0) {
38 return self
::$mock_memcache->set($key, $value, $flag, $expire);
42 // Mock memcached class, used when invalidating cache entries on write.
44 // Mock object to validate calls to memcached
45 static $mock_memcached = null;
47 public static function setMockMemcached($mock) {
48 self
::$mock_memcached = $mock;
51 public function deleteMulti($keys, $time = 0) {
52 self
::$mock_memcached->deleteMulti($keys, $time);
58 namespace google\appengine\ext\cloud_storage_streams
{
60 require_once 'google/appengine/api/app_identity/app_identity_service_pb.php';
61 require_once 'google/appengine/api/app_identity/AppIdentityService.php';
62 require_once 'google/appengine/api/urlfetch_service_pb.php';
63 require_once 'google/appengine/ext/cloud_storage_streams/CloudStorageClient.php';
64 require_once 'google/appengine/ext/cloud_storage_streams/CloudStorageReadClient.php';
65 require_once 'google/appengine/ext/cloud_storage_streams/CloudStorageStreamWrapper.php';
66 require_once 'google/appengine/ext/cloud_storage_streams/CloudStorageWriteClient.php';
67 require_once 'google/appengine/testing/ApiProxyTestBase.php';
69 use google\appengine\testing\ApiProxyTestBase
;
70 use google\appengine\ext\cloud_storage_streams\CloudStorageClient
;
71 use google\appengine\ext\cloud_storage_streams\CloudStorageReadClient
;
72 use google\appengine\ext\cloud_storage_streams\CloudStorageWriteClient
;
73 use google\appengine\ext\cloud_storage_streams\HttpResponse
;
74 use google\appengine\URLFetchRequest\RequestMethod
;
76 class CloudStorageStreamWrapperTest
extends ApiProxyTestBase
{
78 public static $allowed_gs_bucket = "";
80 protected function setUp() {
82 $this->_SERVER
= $_SERVER;
84 stream_wrapper_register("gs",
85 "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
88 CloudStorageStreamWrapperTest
::$allowed_gs_bucket = "";
90 // By default disable caching so we don't have to mock out memcache in
92 stream_context_set_default(['gs' => ['enable_cache' => false]]);
94 date_default_timezone_set("UTC");
96 $this->mock_memcache
= $this->getMock('\Memcache');
97 $this->mock_memcache_call_index
= 0;
98 \Memcache
::setMockMemcache($this->mock_memcache
);
100 $this->mock_memcached
= $this->getMock('\Memcached');
101 \Memcached
::setMockMemcached($this->mock_memcached
);
104 protected function tearDown() {
105 stream_wrapper_unregister("gs");
107 $_SERVER = $this->_SERVER
;
111 public function testInvalidPathName() {
112 $this->setExpectedException("\PHPUnit_Framework_Error");
113 $this->assertFalse(fopen("gs:///object.png", "r"));
114 $this->setExpectedException("\PHPUnit_Framework_Error");
115 $this->assertFalse(fopen("gs://", "r"));
116 $invalid_bucket_names = [
118 '.another_bad_bucket',
121 str_repeat('a', 224),
123 'foobar' . str_repeat('a', 64)
125 foreach ($invalid_bucket_names as $invalid_name) {
126 $this->setExpectedException(
127 "\PHPUnit_Framework_Error",
128 sprintf("Invalid cloud storage bucket name '%s'", $invalid_name));
129 $this->assertFalse(fopen(sprintf('gs://%s/file.txt', $invalid_name),
134 public function testInvalidMode() {
135 $valid_path = "gs://bucket/object_name.png";
136 $this->setExpectedException("\PHPUnit_Framework_Error");
137 $this->assertFalse(fopen($valid_path, "r+"));
138 $this->setExpectedException("\PHPUnit_Framework_Error");
139 $this->assertFalse(fopen($valid_path, "w+"));
140 $this->setExpectedException("\PHPUnit_Framework_Error");
141 $this->assertFalse(fopen($valid_path, "a"));
142 $this->setExpectedException("\PHPUnit_Framework_Error");
143 $this->assertFalse(fopen($valid_path, "a+"));
144 $this->setExpectedException("\PHPUnit_Framework_Error");
145 $this->assertFalse(fopen($valid_path, "x+"));
146 $this->setExpectedException("\PHPUnit_Framework_Error");
147 $this->assertFalse(fopen($valid_path, "c"));
148 $this->setExpectedException("\PHPUnit_Framework_Error");
149 $this->assertFalse(fopen($valid_path, "c+"));
152 public function testReadObjectSuccess() {
153 $body = "Hello from PHP";
155 $this->expectFileReadRequest($body,
157 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
160 $valid_path = "gs://bucket/object_name.png";
161 $data = file_get_contents($valid_path);
163 $this->assertEquals($body, $data);
164 $this->apiProxyMock
->verify();
167 public function testReadObjectCacheHitSuccess() {
168 $body = "Hello from PHP";
170 // First call is to create the OAuth token.
171 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
173 // Second call is to retrieve the cached read.
175 'status_code' => 200,
177 'Content-Length' => strlen($body),
178 'ETag' => 'deadbeef',
179 'Content-Type' => 'text/plain',
180 'Last-Modified' => 'Mon, 02 Jul 2012 01:41:01 GMT',
184 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
186 ->with($this->stringStartsWith('_ah_gs_read_cache'))
187 ->will($this->returnValue($response));
189 // We now expect a read request with If-None-Modified set to our etag.
191 'Authorization' => 'OAuth foo token',
192 'Range' => sprintf('bytes=%d-%d',
194 CloudStorageReadClient
::DEFAULT_READ_SIZE
- 1),
195 'If-None-Match' => 'deadbeef',
196 'x-goog-api-version' => 2,
199 'status_code' => HttpResponse
::NOT_MODIFIED
,
204 $expected_url = $this->makeCloudStorageObjectUrl();
205 $this->expectHttpRequest($expected_url,
211 $options = [ 'gs' => [
212 'enable_cache' => true,
213 'enable_optimistic_cache' => false,
216 $ctx = stream_context_create($options);
217 $valid_path = "gs://bucket/object.png";
218 $data = file_get_contents($valid_path, false, $ctx);
220 $this->assertEquals($body, $data);
221 $this->apiProxyMock
->verify();
224 public function testReadObjectCacheWriteSuccess() {
225 $body = "Hello from PHP";
227 $this->expectFileReadRequest($body,
229 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
232 // Don't read the page from the cache
233 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
235 ->with($this->stringStartsWith('_ah_gs_read_cache'))
236 ->will($this->returnValue(false));
238 // Expect a write back to the cache
239 $cache_expiry_seconds = 60;
240 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
242 ->with($this->stringStartsWith('_ah_gs_read_cache'),
245 $cache_expiry_seconds)
246 ->will($this->returnValue(false));
249 $options = [ 'gs' => [
250 'enable_cache' => true,
251 'enable_optimistic_cache' => false,
252 'read_cache_expiry_seconds' => $cache_expiry_seconds,
255 $ctx = stream_context_create($options);
256 $valid_path = "gs://bucket/object_name.png";
257 $data = file_get_contents($valid_path, false, $ctx);
259 $this->assertEquals($body, $data);
260 $this->apiProxyMock
->verify();
263 public function testReadObjectOptimisiticCacheHitSuccess() {
264 $body = "Hello from PHP";
266 // First call is to create the OAuth token.
267 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
269 // Second call is to retrieve the cached read.
271 'status_code' => 200,
273 'Content-Length' => strlen($body),
274 'ETag' => 'deadbeef',
275 'Content-Type' => 'text/plain',
276 'Last-Modified' => 'Mon, 02 Jul 2012 01:41:01 GMT',
280 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
282 ->with($this->stringStartsWith('_ah_gs_read_cache'))
283 ->will($this->returnValue($response));
285 $options = [ 'gs' => [
286 'enable_cache' => true,
287 'enable_optimistic_cache' => true,
290 $ctx = stream_context_create($options);
291 $valid_path = "gs://bucket/object_name.png";
292 $data = file_get_contents($valid_path, false, $ctx);
294 $this->assertEquals($body, $data);
295 $this->apiProxyMock
->verify();
298 public function testReadObjectPartialContentResponseSuccess() {
299 // GCS returns a 206 even if you can obtain all of the file in the first
300 // read - this test simulates that behavior.
301 $body = "Hello from PHP.";
303 $this->expectFileReadRequest($body,
305 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
309 $valid_path = "gs://bucket/object_name.png";
310 $data = file_get_contents($valid_path);
312 $this->assertEquals($body, $data);
313 $this->apiProxyMock
->verify();
316 public function testReadLargeObjectSuccess() {
317 $body = str_repeat("1234567890", 100000);
318 $data_len = strlen($body);
320 $read_chunks = ceil($data_len / CloudStorageReadClient
::DEFAULT_READ_SIZE
);
324 for ($i = 0; $i < $read_chunks; $i++
) {
325 $this->expectFileReadRequest($body,
327 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
330 $start_chunk +
= CloudStorageReadClient
::DEFAULT_READ_SIZE
;
334 $valid_path = "gs://bucket/object_name.png";
335 $fp = fopen($valid_path, "rt");
336 $data = stream_get_contents($fp);
339 $this->assertEquals($body, $data);
340 $this->apiProxyMock
->verify();
343 public function testSeekReadObjectSuccess() {
344 $body = "Hello from PHP";
346 $this->expectFileReadRequest($body,
348 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
351 $valid_path = "gs://bucket/object_name.png";
352 $fp = fopen($valid_path, "r");
353 $this->assertEquals(0, fseek($fp, 4, SEEK_SET
));
354 $this->assertEquals($body[4], fread($fp, 1));
355 $this->assertEquals(-1, fseek($fp, 100, SEEK_SET
));
356 $this->assertTrue(fclose($fp));
358 $this->apiProxyMock
->verify();
361 public function testReadZeroSizedObjectSuccess() {
362 $this->expectFileReadRequest("",
364 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
367 $data = file_get_contents("gs://bucket/object_name.png");
369 $this->assertEquals("", $data);
370 $this->apiProxyMock
->verify();
373 public function testFileSizeSucess() {
374 $body = "Hello from PHP";
376 $this->expectFileReadRequest($body,
378 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
381 $valid_path = "gs://bucket/object_name.png";
382 $fp = fopen($valid_path, "r");
385 $this->assertEquals(strlen($body), $stat["size"]);
386 $this->apiProxyMock
->verify();
389 public function testDeleteObjectSuccess() {
390 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
392 $request_headers = $this->getStandardRequestHeaders();
394 'status_code' => 204,
398 $expected_url = $this->makeCloudStorageObjectUrl("my_bucket",
400 $this->expectHttpRequest($expected_url,
401 RequestMethod
::DELETE
,
406 $this->assertTrue(unlink("gs://my_bucket/some%file.txt"));
407 $this->apiProxyMock
->verify();
410 public function testDeleteObjectFail() {
411 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
413 $request_headers = $this->getStandardRequestHeaders();
415 'status_code' => 404,
418 'body' => "<?xml version='1.0' encoding='utf-8'?>
420 <Code>NoSuchBucket</Code>
421 <Message>No Such Bucket</Message>
424 $expected_url = $this->makeCloudStorageObjectUrl();
425 $this->expectHttpRequest($expected_url,
426 RequestMethod
::DELETE
,
431 $this->setExpectedException("PHPUnit_Framework_Error_Warning",
432 "Cloud Storage Error: No Such Bucket (NoSuchBucket)");
433 $this->assertFalse(unlink("gs://bucket/object.png"));
434 $this->apiProxyMock
->verify();
437 public function testStatBucketSuccess() {
438 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
439 $request_headers = $this->getStandardRequestHeaders();
440 $file_results = ['file1.txt', 'file2.txt'];
442 'status_code' => 200,
445 'body' => $this->makeGetBucketXmlResponse("", $file_results),
447 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
448 $expected_query = http_build_query([
449 "delimiter" => CloudStorageClient
::DELIMITER
,
450 "max-keys" => CloudStorageUrlStatClient
::MAX_KEYS
,
453 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
459 // Return a false is writable check from the cache
460 $this->expectIsWritableMemcacheLookup(true, false);
462 $this->assertTrue(is_dir("gs://bucket"));
463 $this->apiProxyMock
->verify();
466 public function testStatObjectSuccess() {
467 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
468 // Return the object we want in the second request so we test fetching from
469 // the marker to get all of the results
470 $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
471 $request_headers = $this->getStandardRequestHeaders();
473 ['key' => 'object1.png', 'size' => '3337', 'mtime' => $last_modified],
476 'status_code' => 200,
479 'body' => $this->makeGetBucketXmlResponse("", $file_results, "foo"),
481 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
482 $expected_query = http_build_query([
483 'delimiter' => CloudStorageClient
::DELIMITER
,
484 'max-keys' => CloudStorageUrlStatClient
::MAX_KEYS
,
485 'prefix' => 'object.png',
488 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
494 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
496 ['key' => 'object.png', 'size' => '37337', 'mtime' => $last_modified],
498 $response['body'] = $this->makeGetBucketXmlResponse("", $file_results);
499 $expected_query = http_build_query([
500 'delimiter' => CloudStorageClient
::DELIMITER
,
501 'max-keys' => CloudStorageUrlStatClient
::MAX_KEYS
,
502 'prefix' => 'object.png',
505 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
511 // Don't find the key in the cache, to force a write attempt to the bucket.
512 $temp_url = $this->makeCloudStorageObjectUrl("bucket",
513 CloudStorageClient
::WRITABLE_TEMP_FILENAME
);
514 $this->expectIsWritableMemcacheLookup(false, false);
515 $this->expectFileWriteStartRequest(null, null, 'foo', $temp_url, null);
516 $this->expectIsWritableMemcacheSet(true);
519 $result = stat("gs://bucket/object.png");
520 $this->assertEquals(37337, $result['size']);
521 $this->assertEquals(0100666, $result['mode']);
522 $this->assertEquals(strtotime($last_modified), $result['mtime']);
523 $this->apiProxyMock
->verify();
526 public function testStatObjectAsFolderSuccess() {
527 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
528 $request_headers = $this->getStandardRequestHeaders();
529 $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
531 ['key' => 'a/b_$folder$', 'size' => '0', 'mtime' => $last_modified],
534 'status_code' => 200,
537 'body' => $this->makeGetBucketXmlResponse('a/b', $file_results),
539 $expected_url = $this->makeCloudStorageObjectUrl('bucket', null);
540 $expected_query = http_build_query([
541 'delimiter' => CloudStorageClient
::DELIMITER
,
542 'max-keys' => CloudStorageUrlStatClient
::MAX_KEYS
,
546 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
551 // Return a false is writable check from the cache
552 $this->expectIsWritableMemcacheLookup(true, false);
554 $this->assertTrue(is_dir('gs://bucket/a/b/'));
555 $this->apiProxyMock
->verify();
558 public function testStatObjectWithCommonPrefixSuccess() {
559 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
560 $request_headers = $this->getStandardRequestHeaders();
561 $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
562 $common_prefix_results = ['a/b/c/',
566 'status_code' => 200,
569 'body' => $this->makeGetBucketXmlResponse('a/b',
572 $common_prefix_results),
574 $expected_url = $this->makeCloudStorageObjectUrl('bucket', null);
575 $expected_query = http_build_query([
576 'delimiter' => CloudStorageClient
::DELIMITER
,
577 'max-keys' => CloudStorageUrlStatClient
::MAX_KEYS
,
581 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
586 // Return a false is writable check from the cache
587 $this->expectIsWritableMemcacheLookup(true, false);
589 $this->assertTrue(is_dir('gs://bucket/a/b'));
590 $this->apiProxyMock
->verify();
593 public function testStatObjectFailed() {
594 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
595 $request_headers = $this->getStandardRequestHeaders();
597 'status_code' => 404,
601 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
602 $expected_query = http_build_query([
603 'delimiter' => CloudStorageClient
::DELIMITER
,
604 'max-keys' => CloudStorageUrlStatClient
::MAX_KEYS
,
605 'prefix' => 'object.png',
608 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
614 $this->setExpectedException("PHPUnit_Framework_Error_Warning");
615 $result = stat("gs://bucket/object.png");
616 $this->apiProxyMock
->verify();
619 public function testInvalidRenamePaths() {
620 $this->setExpectedException("\PHPUnit_Framework_Error");
621 $this->assertFalse(rename("gs://bucket/object.png", "gs://to/"));
622 $this->setExpectedException("\PHPUnit_Framework_Error");
623 $this->assertFalse(rename("gs://bucket/", "gs://to/object.png"));
626 public function testRenameObjectSuccess() {
627 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
629 // First there is a stat
630 $request_headers = $this->getStandardRequestHeaders();
632 'status_code' => 200,
634 'Content-Length' => 37337,
636 'Content-Type' => 'text/plain',
640 $expected_url = $this->makeCloudStorageObjectUrl();
641 $this->expectHttpRequest($expected_url,
647 // Then there is a copy
649 "Authorization" => "OAuth foo token",
650 "x-goog-copy-source" => '/bucket/object.png',
651 "x-goog-copy-source-if-match" => 'abcdef',
652 "content-type" => 'text/plain',
653 "x-goog-metadata-directive" => "COPY",
654 "x-goog-api-version" => 2,
657 'status_code' => 200,
661 $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to.png");
662 $this->expectHttpRequest($expected_url,
668 // Then we unlink the original.
669 $request_headers = $this->getStandardRequestHeaders();
671 'status_code' => 204,
675 $expected_url = $this->makeCloudStorageObjectUrl();
676 $this->expectHttpRequest($expected_url,
677 RequestMethod
::DELETE
,
682 $from = "gs://bucket/object.png";
683 $to = "gs://to_bucket/to.png";
685 $this->assertTrue(rename($from, $to));
686 $this->apiProxyMock
->verify();
689 public function testRenameObjectFromObjectNotFound() {
690 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
692 // First there is a stat
693 $request_headers = $this->getStandardRequestHeaders();
695 'status_code' => 404,
700 $expected_url = $this->makeCloudStorageObjectUrl();
701 $this->expectHttpRequest($expected_url,
707 $from = "gs://bucket/object.png";
708 $to = "gs://to_bucket/to_object";
710 $this->setExpectedException("\PHPUnit_Framework_Error_Warning");
712 $this->apiProxyMock
->verify();
715 public function testRenameObjectCopyFailed() {
716 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
718 // First there is a stat
719 $request_headers = $this->getStandardRequestHeaders();
721 'status_code' => 200,
723 'Content-Length' => 37337,
725 'Content-Type' => 'text/plain',
729 $expected_url = $this->makeCloudStorageObjectUrl();
730 $this->expectHttpRequest($expected_url,
736 // Then there is a copy
738 "Authorization" => "OAuth foo token",
739 "x-goog-copy-source" => '/bucket/object.png',
740 "x-goog-copy-source-if-match" => 'abcdef',
741 "content-type" => 'text/plain',
742 "x-goog-metadata-directive" => "COPY",
743 "x-goog-api-version" => 2,
746 'status_code' => 412,
750 $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to_object");
751 $this->expectHttpRequest($expected_url,
757 $from = "gs://bucket/object.png";
758 $to = "gs://to_bucket/to_object";
760 $this->setExpectedException("\PHPUnit_Framework_Error_Warning");
761 $this->assertFalse(rename($from, $to));
762 $this->apiProxyMock
->verify();
765 public function testRenameObjectUnlinkFailed() {
766 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
768 // First there is a stat
769 $request_headers = $this->getStandardRequestHeaders();
771 'status_code' => 200,
773 'Content-Length' => 37337,
775 'Content-Type' => 'text/plain',
779 $expected_url = $this->makeCloudStorageObjectUrl();
780 $this->expectHttpRequest($expected_url,
786 // Then there is a copy
788 "Authorization" => "OAuth foo token",
789 "x-goog-copy-source" => '/bucket/object.png',
790 "x-goog-copy-source-if-match" => 'abcdef',
791 "content-type" => 'text/plain',
792 "x-goog-metadata-directive" => "COPY",
793 "x-goog-api-version" => 2,
796 'status_code' => 200,
800 $expected_url = $this->makeCloudStorageObjectUrl("to_bucket",
802 $this->expectHttpRequest($expected_url,
808 // Then we unlink the original.
809 $request_headers = $this->getStandardRequestHeaders();
811 'status_code' => 404,
815 $expected_url = $this->makeCloudStorageObjectUrl();
816 $this->expectHttpRequest($expected_url,
817 RequestMethod
::DELETE
,
822 $from = "gs://bucket/object.png";
823 $to = "gs://to_bucket/to_object";
825 $this->setExpectedException("\PHPUnit_Framework_Error_Warning");
826 $this->assertFalse(rename($from, $to));
827 $this->apiProxyMock
->verify();
830 public function testWriteObjectSuccess() {
831 $this->writeObjectSuccessWithMetadata("Hello To PHP.");
834 public function testWriteObjectWithMetadata() {
835 $metadata = ["foo" => "far", "bar" => "boo"];
836 $this->writeObjectSuccessWithMetadata("Goodbye To PHP.", $metadata);
839 private function writeObjectSuccessWithMetadata($data, $metadata = NULL) {
840 $data_len = strlen($data);
841 $expected_url = $this->makeCloudStorageObjectUrl();
842 $this->expectFileWriteStartRequest("text/plain",
848 $this->expectFileWriteContentRequest($expected_url,
856 "acl" => "public-read",
857 "Content-Type" => "text/plain",
858 'enable_cache' => true,
861 if (isset($metadata)) {
862 $context["gs"]["metadata"] = $metadata;
865 $range = sprintf("bytes=0-%d", CloudStorageClient
::DEFAULT_READ_SIZE
- 1);
866 $cache_key = sprintf(CloudStorageClient
::MEMCACHE_KEY_FORMAT
,
869 $this->mock_memcached
->expects($this->once())
870 ->method('deleteMulti')
871 ->with($this->identicalTo([$cache_key]));
873 stream_context_set_default($context);
874 $this->assertEquals($data_len,
875 file_put_contents("gs://bucket/object.png", $data));
876 $this->apiProxyMock
->verify();
879 public function testWriteInvalidMetadata() {
880 $metadata = ["f o o" => "far"];
883 "acl" => "public-read",
884 "Content-Type" => "text/plain",
885 "metadata" => $metadata
888 stream_context_set_default($context);
889 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
890 $this->setExpectedException("\PHPUnit_Framework_Error");
891 file_put_contents("gs://bucket/object.png", "Some data");
894 public function testWriteLargeObjectSuccess() {
895 $data_to_write = str_repeat("1234567890", 100000);
896 $data_len = strlen($data_to_write);
898 $expected_url = $this->makeCloudStorageObjectUrl();
900 $this->expectFileWriteStartRequest("text/plain",
905 $chunks = floor($data_len / CloudStorageWriteClient
::WRITE_CHUNK_SIZE
);
907 $end_byte = CloudStorageWriteClient
::WRITE_CHUNK_SIZE
- 1;
909 for ($i = 0 ; $i < $chunks ; $i++
) {
910 $this->expectFileWriteContentRequest($expected_url,
916 $start_byte +
= CloudStorageWriteClient
::WRITE_CHUNK_SIZE
;
917 $end_byte +
= CloudStorageWriteClient
::WRITE_CHUNK_SIZE
;
920 // Write out the remainder
921 $this->expectFileWriteContentRequest($expected_url,
930 "acl" => "public-read",
931 "Content-Type" => "text/plain",
932 'enable_cache' => true,
937 for ($i = 0; $i < $data_len; $i +
= CloudStorageClient
::DEFAULT_READ_SIZE
) {
938 $range = sprintf("bytes=%d-%d",
940 $i + CloudStorageClient
::DEFAULT_READ_SIZE
- 1);
941 $delete_keys[] = sprintf(CloudStorageClient
::MEMCACHE_KEY_FORMAT
,
945 $this->mock_memcached
->expects($this->once())
946 ->method('deleteMulti')
947 ->with($this->identicalTo($delete_keys));
949 $ctx = stream_context_create($file_context);
950 $this->assertEquals($data_len,
951 file_put_contents("gs://bucket/object.png",
955 $this->apiProxyMock
->verify();
958 public function testWriteEmptyObjectSuccess() {
962 $expected_url = $this->makeCloudStorageObjectUrl("bucket",
965 $this->expectFileWriteStartRequest("text/plain",
970 $this->expectFileWriteContentRequest($expected_url,
975 true); // Complete write
979 "acl" => "public-read",
980 "Content-Type" => "text/plain",
983 $ctx = stream_context_create($file_context);
984 $fp = fopen("gs://bucket/empty_file.txt", "wt", false, $ctx);
985 $this->assertEquals($data_len, fwrite($fp, $data_to_write));
987 $this->apiProxyMock
->verify();
990 public function testInvalidBucketForInclude() {
991 stream_wrapper_unregister("gs");
992 stream_wrapper_register("gs",
993 "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
996 $this->setExpectedException("\PHPUnit_Framework_Error");
997 include 'gs://bucket/object.php';
1000 public function testValidBucketForInclude() {
1001 stream_wrapper_unregister("gs");
1002 stream_wrapper_register("gs",
1003 "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
1006 $body = '<?php $a = "foo";';
1008 $this->expectFileReadRequest($body,
1010 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
1013 define("GAE_INCLUDE_GS_BUCKETS", "foo, bucket, bar");
1014 $valid_path = "gs://bucket/object_name.png";
1015 require $valid_path;
1017 $this->assertEquals($a, 'foo');
1018 $this->apiProxyMock
->verify();
1021 public function testInvalidPathForOpenDir() {
1022 $this->setExpectedException("\PHPUnit_Framework_Error");
1023 $this->assertFalse(opendir("gs:///"));
1024 $this->setExpectedException("\PHPUnit_Framework_Error");
1025 $this->assertFalse(fopen("gs://"));
1028 public function testReaddirSuccess() {
1029 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1031 $request_headers = $this->getStandardRequestHeaders();
1032 $file_results = ['f/file1.txt', 'f/file2.txt', 'f/sub_$folder$'];
1034 'status_code' => 200,
1037 'body' => $this->makeGetBucketXmlResponse("f/", $file_results),
1039 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
1040 $expected_query = http_build_query([
1041 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1042 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1046 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1052 $res = opendir("gs://bucket/f");
1053 $this->assertEquals("file1.txt", readdir($res));
1054 $this->assertEquals("file2.txt", readdir($res));
1055 $this->assertEquals("sub/", readdir($res));
1056 $this->assertFalse(readdir($res));
1058 $this->apiProxyMock
->verify();
1061 public function testReaddirTruncatedSuccess() {
1062 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1063 $request_headers = $this->getStandardRequestHeaders();
1064 // First query with a truncated response
1065 $response_body = "<?xml version='1.0' encoding='UTF-8'?>
1066 <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
1067 <Name>sjl-test</Name>
1070 <NextMarker>AA</NextMarker>
1071 <Delimiter>/</Delimiter>
1072 <IsTruncated>true</IsTruncated>
1074 <Key>f/file1.txt</Key>
1077 <Key>f/file2.txt</Key>
1079 </ListBucketResult>";
1081 'status_code' => 200,
1084 'body' => $response_body,
1086 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
1087 $expected_query = http_build_query([
1088 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1089 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1093 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1099 // Second query with the remaining response
1100 $response_body = "<?xml version='1.0' encoding='UTF-8'?>
1101 <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
1102 <Name>sjl-test</Name>
1105 <Delimiter>/</Delimiter>
1106 <IsTruncated>false</IsTruncated>
1108 <Key>f/file3.txt</Key>
1111 <Key>f/file4.txt</Key>
1113 </ListBucketResult>";
1115 'status_code' => 200,
1118 'body' => $response_body,
1121 $expected_query = http_build_query([
1122 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1123 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1128 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1129 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1135 $res = opendir("gs://bucket/f");
1136 $this->assertEquals("file1.txt", readdir($res));
1137 $this->assertEquals("file2.txt", readdir($res));
1138 $this->assertEquals("file3.txt", readdir($res));
1139 $this->assertEquals("file4.txt", readdir($res));
1140 $this->assertFalse(readdir($res));
1142 $this->apiProxyMock
->verify();
1145 public function testRewindDirSuccess() {
1146 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1147 $request_headers = $this->getStandardRequestHeaders();
1149 'status_code' => 200,
1152 'body' => $this->makeGetBucketXmlResponse(
1154 ["f/file1.txt", "f/file2.txt"]),
1156 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
1157 $expected_query = http_build_query([
1158 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1159 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1163 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1168 // Expect the requests again when we rewinddir
1169 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1170 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1176 $res = opendir("gs://bucket/f");
1177 $this->assertEquals("file1.txt", readdir($res));
1179 $this->assertEquals("file1.txt", readdir($res));
1180 $this->assertEquals("file2.txt", readdir($res));
1181 $this->assertFalse(readdir($res));
1183 $this->apiProxyMock
->verify();
1186 public function testInvalidPathForMkDir() {
1187 $this->setExpectedException("\PHPUnit_Framework_Error");
1188 $this->assertFalse(mkdir("gs://bucket_without_object/"));
1189 $this->setExpectedException("\PHPUnit_Framework_Error");
1190 $this->assertFalse(mkdir("gs://"));
1193 public function testMkDirSuccess() {
1194 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
1195 $request_headers = [
1196 "Authorization" => "OAuth foo token",
1197 "x-goog-if-generation-match" => 0,
1198 "Content-Range" => "bytes */0",
1199 "x-goog-api-version" => 2,
1203 'status_code' => 200,
1208 $expected_url = $this->makeCloudStorageObjectUrl('bucket',
1209 '/dira/dirb_$folder$');
1210 $this->expectHttpRequest($expected_url,
1216 $this->assertTrue(mkdir("gs://bucket/dira/dirb"));
1217 $this->apiProxyMock
->verify();
1220 public function testInvalidPathForRmDir() {
1221 $this->setExpectedException("\PHPUnit_Framework_Error");
1222 $this->assertFalse(rmdir("gs://bucket_without_object/"));
1223 $this->setExpectedException("\PHPUnit_Framework_Error");
1224 $this->assertFalse(rmdir("gs://"));
1227 public function testRmDirSuccess() {
1228 // Expect a request to list the contents of the bucket to ensure that it is
1230 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1231 $request_headers = $this->getStandardRequestHeaders();
1232 // First query with a truncated response
1234 'status_code' => 200,
1237 'body' => $this->makeGetBucketXmlResponse("dira/dirb/", []),
1239 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
1240 $expected_query = http_build_query([
1241 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1242 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1243 "prefix" => "dira/dirb/",
1246 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1252 // Expect the unlink request for the folder.
1253 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
1254 $request_headers = $this->getStandardRequestHeaders();
1256 'status_code' => 204,
1261 $expected_url = $this->makeCloudStorageObjectUrl('bucket',
1262 '/dira/dirb_$folder$');
1263 $this->expectHttpRequest($expected_url,
1264 RequestMethod
::DELETE
,
1269 $this->assertTrue(rmdir("gs://bucket/dira/dirb"));
1270 $this->apiProxyMock
->verify();
1273 public function testRmDirNotEmpry() {
1274 // Expect a request to list the contents of the bucket to ensure that it is
1276 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1277 $request_headers = $this->getStandardRequestHeaders();
1278 // First query with a truncated response
1280 'status_code' => 200,
1283 'body' => $this->makeGetBucketXmlResponse(
1285 ["dira/dirb/file1.txt"]),
1287 $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
1288 $expected_query = http_build_query([
1289 "delimiter" => CloudStorageDirectoryClient
::DELIMITER
,
1290 "max-keys" => CloudStorageDirectoryClient
::MAX_KEYS
,
1291 "prefix" => "dira/dirb/",
1294 $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
1300 $this->setExpectedException("\PHPUnit_Framework_Error");
1301 $this->assertFalse(rmdir("gs://bucket/dira/dirb"));
1302 $this->apiProxyMock
->verify();
1305 public function testStreamCast() {
1306 $body = "Hello from PHP";
1308 $this->expectFileReadRequest($body,
1310 CloudStorageReadClient
::DEFAULT_READ_SIZE
,
1313 $valid_path = "gs://bucket/object_name.png";
1314 $this->setExpectedException(
1315 '\PHPUnit_Framework_Error',
1316 'gzopen(): cannot represent a stream of type user-space as a ' .
1318 $this->assertFalse(gzopen($valid_path, 'rb'));
1319 $this->apiProxyMock
->verify();
1322 private function expectFileReadRequest($body,
1326 $paritial_content = null) {
1327 $this->expectGetAccessTokenRequest(CloudStorageClient
::READ_SCOPE
);
1329 assert($length > 0);
1330 $last_byte = $start_byte +
$length - 1;
1331 $request_headers = [
1332 "Authorization" => "OAuth foo token",
1333 "Range" => sprintf("bytes=%d-%d", $start_byte, $last_byte),
1337 $request_headers['If-Match'] = $etag;
1340 $request_headers["x-goog-api-version"] = 2;
1342 $response_headers = [
1343 "ETag" => "deadbeef",
1344 "Content-Type" => "text/plain",
1345 "Last-Modified" => "Mon, 02 Jul 2012 01:41:01 GMT",
1348 $response = $this->createSuccessfulGetHttpResponse($response_headers,
1354 $exected_url = self
::makeCloudStorageObjectUrl("bucket",
1355 "/object_name.png");
1357 $this->expectHttpRequest($exected_url,
1364 private function expectGetAccessTokenRequest($scope) {
1365 $req = new \google\appengine\
GetAccessTokenRequest();
1367 $req->addScope($scope);
1369 $resp = new \google\appengine\
GetAccessTokenResponse();
1370 $resp->setAccessToken('foo token');
1371 $resp->setExpirationTime(12345);
1373 $this->apiProxyMock
->expectCall('app_identity_service',
1378 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
1380 ->with($this->stringStartsWith('_ah_app_identity'))
1381 ->will($this->returnValue(false));
1383 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
1385 ->with($this->stringStartsWith('_ah_app_identity'),
1389 ->will($this->returnValue(false));
1392 private function createSuccessfulGetHttpResponse($headers,
1396 $return_partial_content) {
1397 $total_body_length = strlen($body);
1398 $partial_content = false;
1399 $range_cannot_be_satisfied = false;
1401 if ($total_body_length <= $start_byte) {
1402 $range_cannot_be_satisfied = true;
1403 $body = "<Message>The requested range cannot be satisfied.</Message>";
1405 if ($start_byte != 0 ||
$length < $total_body_length) {
1406 $final_length = min($length, $total_body_length - $start_byte);
1407 $body = substr($body, $start_byte, $final_length);
1408 $partial_content = true;
1409 } else if ($return_partial_content) {
1410 $final_length = strlen($body);
1411 $partial_content = true;
1415 $success_headers = [];
1416 if ($range_cannot_be_satisfied) {
1417 $status_code = HttpResponse
::RANGE_NOT_SATISFIABLE
;
1418 $success_headers["Content-Length"] = $total_body_length;
1419 } else if (!$partial_content) {
1420 $status_code = HttpResponse
::OK
;
1421 $success_headers["Content-Length"] = $total_body_length;
1423 $status_code = HttpResponse
::PARTIAL_CONTENT
;
1424 $end_range = $start_byte +
$final_length - 1;
1425 $success_headers["Content-Length"] = $final_length;
1426 $success_headers["Content-Range"] = sprintf("bytes %d-%d/%d",
1429 $total_body_length);
1433 'status_code' => $status_code,
1434 'headers' => array_merge($success_headers, $headers),
1439 private function expectFileWriteStartRequest($content_type,
1444 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
1445 $upload_id = "https://host/bucket/object.png?upload_id=" . $id;
1446 // The upload will start with a POST to acquire the upload ID.
1447 $request_headers = [
1448 "x-goog-resumable" => "start",
1449 "Authorization" => "OAuth foo token",
1451 if ($content_type != null) {
1452 $request_headers['Content-Type'] = $content_type;
1455 $request_headers['x-goog-acl'] = $acl;
1457 if (isset($metadata)) {
1458 foreach ($metadata as $key => $value) {
1459 $request_headers["x-goog-meta-" . $key] = $value;
1462 $request_headers["x-goog-api-version"] = 2;
1464 'status_code' => 201,
1466 'Location' => $upload_id,
1469 $this->expectHttpRequest($url,
1470 RequestMethod
::POST
,
1476 private function expectFileWriteContentRequest($url,
1482 // The upload will be completed with a PUT with the final length
1483 $this->expectGetAccessTokenRequest(CloudStorageClient
::WRITE_SCOPE
);
1484 // If start byte is null then we assume that this is a PUT with no content,
1485 // and the end_byte contains the length of the data to write.
1486 if (is_null($start_byte)) {
1487 $range = sprintf("bytes */%d", $end_byte);
1488 $status_code = HttpResponse
::OK
;
1491 $length = $end_byte - $start_byte +
1;
1493 $total_len = $end_byte +
1;
1494 $range = sprintf("bytes %d-%d/%d", $start_byte, $end_byte, $total_len);
1495 $status_code = HttpResponse
::OK
;
1497 $range = sprintf("bytes %d-%d/*", $start_byte, $end_byte);
1498 $status_code = HttpResponse
::RESUME_INCOMPLETE
;
1500 $body = substr($data, $start_byte, $length);
1502 $request_headers = [
1503 "Authorization" => "OAuth foo token",
1504 "Content-Range" => $range,
1505 "x-goog-api-version" => 2,
1508 'status_code' => $status_code,
1512 $expected_url = $url . "?upload_id=" . $upload_id;
1513 $this->expectHttpRequest($expected_url,
1520 private function expectHttpRequest($url, $method, $headers, $body, $result) {
1521 $req = new \google\appengine\
URLFetchRequest();
1523 $req->setMethod($method);
1524 $req->setMustValidateServerCertificate(true);
1526 foreach($headers as $k => $v) {
1527 $h = $req->addHeader();
1533 $req->setPayload($body);
1536 $resp = new \google\appengine\
URLFetchResponse();
1538 $resp->setStatusCode($result['status_code']);
1539 foreach($result['headers'] as $k => $v) {
1540 $h = $resp->addHeader();
1544 if (isset($result['body'])) {
1545 $resp->setContent($result['body']);
1548 $this->apiProxyMock
->expectCall('urlfetch',
1554 private function expectIsWritableMemcacheLookup($key_found, $result) {
1556 $lookup_result = ['is_writable' => $result];
1558 $lookup_result = false;
1561 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
1563 ->with($this->stringStartsWith(
1564 '_ah_gs_write_bucket_cache_'))
1565 ->will($this->returnValue($lookup_result));
1568 private function expectIsWritableMemcacheSet($value) {
1569 $this->mock_memcache
->expects($this->at($this->mock_memcache_call_index++
))
1571 ->with($this->stringStartsWith('_ah_gs_write_bucket_cache_'),
1572 ['is_writable' => $value],
1574 CloudStorageClient
::DEFAULT_WRITABLE_CACHE_EXPIRY_SECONDS
)
1575 ->will($this->returnValue(false));
1578 private function makeCloudStorageObjectUrl($bucket = "bucket",
1579 $object = "/object.png") {
1580 return CloudStorageClient
::createObjectUrl($bucket, $object);
1583 private function getStandardRequestHeaders() {
1585 "Authorization" => "OAuth foo token",
1586 "x-goog-api-version" => 2,
1590 private function makeGetBucketXmlResponse($prefix,
1592 $next_marker = null,
1593 $common_prefix_array = null) {
1594 $result = "<?xml version='1.0' encoding='UTF-8'?>
1595 <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
1596 <Name>sjl-test</Name>
1597 <Prefix>" . $prefix . "</Prefix>
1599 if (isset($next_marker)) {
1600 $result .= "<NextMarker>" . $next_marker . "</NextMarker>";
1602 $result .= "<Delimiter>/</Delimiter>
1603 <IsTruncated>false</IsTruncated>";
1605 foreach($contents_array as $content) {
1606 $result .= '<Contents>';
1607 if (is_string($content)) {
1608 $result .= '<Key>' . $content . '</Key>';
1610 $result .= '<Key>' . $content['key'] . '</Key>';
1611 $result .= '<Size>' . $content['size'] . '</Size>';
1612 $result .= '<LastModified>' . $content['mtime'] . '</LastModified>';
1614 $result .= '</Contents>';
1616 if (isset($common_prefix_array)) {
1617 foreach($common_prefix_array as $common_prefix) {
1618 $result .= '<CommonPrefixes>';
1619 $result .= '<Prefix>' . $common_prefix . '</Prefix>';
1620 $result .= '</CommonPrefixes>';
1623 $result .= "</ListBucketResult>";
1628 } // namespace google\appengine\ext\cloud_storage_streams;