These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace League\Flysystem\AwsS3V3; |
||
6 | |||
7 | use Aws\Result; |
||
8 | use Aws\S3\S3Client; |
||
9 | use Aws\S3\S3ClientInterface; |
||
10 | use Exception; |
||
11 | use Generator; |
||
12 | use League\Flysystem\AdapterTestUtilities\FilesystemAdapterTestCase; |
||
13 | use League\Flysystem\Config; |
||
14 | use League\Flysystem\FileAttributes; |
||
15 | use League\Flysystem\FilesystemAdapter; |
||
16 | use League\Flysystem\StorageAttributes; |
||
17 | use League\Flysystem\UnableToCheckFileExistence; |
||
18 | use League\Flysystem\UnableToDeleteFile; |
||
19 | use League\Flysystem\UnableToMoveFile; |
||
20 | use League\Flysystem\UnableToRetrieveMetadata; |
||
21 | use League\Flysystem\UnableToWriteFile; |
||
22 | use RuntimeException; |
||
23 | |||
24 | /** |
||
25 | * @group aws |
||
26 | */ |
||
27 | class AwsS3V3AdapterTest extends FilesystemAdapterTestCase |
||
28 | { |
||
29 | /** |
||
30 | * @var bool |
||
31 | */ |
||
32 | private $shouldCleanUp = false; |
||
33 | |||
34 | /** |
||
35 | * @var string |
||
36 | */ |
||
37 | private static $adapterPrefix = 'test-prefix'; |
||
38 | |||
39 | /** |
||
40 | * @var S3ClientInterface|null |
||
41 | */ |
||
42 | private static $s3Client; |
||
43 | |||
44 | /** |
||
45 | * @var S3ClientStub |
||
46 | */ |
||
47 | private static $stubS3Client; |
||
48 | |||
49 | public static function setUpBeforeClass(): void |
||
50 | { |
||
51 | static::$adapterPrefix = 'ci/' . bin2hex(random_bytes(10)); |
||
52 | } |
||
53 | |||
54 | protected function tearDown(): void |
||
55 | { |
||
56 | if ( ! $this->shouldCleanUp) { |
||
57 | return; |
||
58 | } |
||
59 | |||
60 | $adapter = $this->adapter(); |
||
61 | $adapter->deleteDirectory('/'); |
||
62 | /** @var StorageAttributes[] $listing */ |
||
63 | $listing = $adapter->listContents('', false); |
||
64 | |||
65 | foreach ($listing as $item) { |
||
66 | if ($item->isFile()) { |
||
67 | $adapter->delete($item->path()); |
||
68 | } else { |
||
69 | $adapter->deleteDirectory($item->path()); |
||
70 | } |
||
71 | } |
||
72 | |||
73 | self::$adapter = null; |
||
74 | } |
||
75 | |||
76 | private static function s3Client(): S3ClientInterface |
||
77 | { |
||
78 | if (static::$s3Client instanceof S3ClientInterface) { |
||
79 | return static::$s3Client; |
||
80 | } |
||
81 | |||
82 | $key = getenv('FLYSYSTEM_AWS_S3_KEY'); |
||
83 | $secret = getenv('FLYSYSTEM_AWS_S3_SECRET'); |
||
84 | $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET'); |
||
85 | $region = getenv('FLYSYSTEM_AWS_S3_REGION') ?: 'eu-central-1'; |
||
86 | |||
87 | if ( ! $key || ! $secret || ! $bucket) { |
||
88 | self::markTestSkipped('No AWS credentials present for testing.'); |
||
89 | } |
||
90 | |||
91 | $options = ['version' => 'latest', 'credentials' => compact('key', 'secret'), 'region' => $region]; |
||
92 | |||
93 | return static::$s3Client = new S3Client($options); |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * @test |
||
98 | */ |
||
99 | public function writing_with_a_specific_mime_type(): void |
||
100 | { |
||
101 | $adapter = $this->adapter(); |
||
102 | $adapter->write('some/path.txt', 'contents', new Config(['ContentType' => 'text/plain+special'])); |
||
103 | $mimeType = $adapter->mimeType('some/path.txt')->mimeType(); |
||
104 | $this->assertEquals('text/plain+special', $mimeType); |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * @test |
||
109 | */ |
||
110 | public function listing_contents_recursive(): void |
||
111 | { |
||
112 | $adapter = $this->adapter(); |
||
113 | $adapter->write('something/0/here.txt', 'contents', new Config()); |
||
114 | $adapter->write('something/1/also/here.txt', 'contents', new Config()); |
||
115 | |||
116 | $contents = iterator_to_array($adapter->listContents('', true)); |
||
117 | |||
118 | $this->assertCount(2, $contents); |
||
119 | $this->assertContainsOnlyInstancesOf(FileAttributes::class, $contents); |
||
120 | /** @var FileAttributes $file */ |
||
121 | $file = $contents[0]; |
||
122 | $this->assertEquals('something/0/here.txt', $file->path()); |
||
123 | /** @var FileAttributes $file */ |
||
124 | $file = $contents[1]; |
||
125 | $this->assertEquals('something/1/also/here.txt', $file->path()); |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * @test |
||
130 | */ |
||
131 | View Code Duplication | public function failing_to_delete_while_moving(): void |
|
0 ignored issues
–
show
|
|||
132 | { |
||
133 | $adapter = $this->adapter(); |
||
134 | $adapter->write('source.txt', 'contents to be copied', new Config()); |
||
135 | static::$stubS3Client->failOnNextCopy(); |
||
136 | |||
137 | $this->expectException(UnableToMoveFile::class); |
||
138 | |||
139 | $adapter->move('source.txt', 'destination.txt', new Config()); |
||
140 | } |
||
141 | |||
142 | |||
143 | |||
144 | /** |
||
145 | * @test |
||
146 | */ |
||
147 | View Code Duplication | public function failing_to_write_a_file(): void |
|
0 ignored issues
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
148 | { |
||
149 | $adapter = $this->adapter(); |
||
150 | static::$stubS3Client->throwDuringUpload(new RuntimeException('Oh no')); |
||
151 | |||
152 | $this->expectException(UnableToWriteFile::class); |
||
153 | |||
154 | $adapter->write('path.txt', 'contents', new Config()); |
||
155 | } |
||
156 | |||
157 | /** |
||
158 | * @test |
||
159 | */ |
||
160 | public function failing_to_delete_a_file(): void |
||
161 | { |
||
162 | $adapter = $this->adapter(); |
||
163 | static::$stubS3Client->throwExceptionWhenExecutingCommand('DeleteObject'); |
||
164 | |||
165 | $this->expectException(UnableToDeleteFile::class); |
||
166 | |||
167 | $adapter->delete('path.txt'); |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * @test |
||
172 | */ |
||
173 | public function fetching_unknown_mime_type_of_a_file(): void |
||
174 | { |
||
175 | $this->adapter(); |
||
176 | $result = new Result([ |
||
177 | 'Key' => static::$adapterPrefix . '/unknown-mime-type.md5', |
||
178 | ]); |
||
179 | static::$stubS3Client->stageResultForCommand('HeadObject', $result); |
||
180 | |||
181 | parent::fetching_unknown_mime_type_of_a_file(); |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * @test |
||
186 | * @dataProvider dpFailingMetadataGetters |
||
187 | */ |
||
188 | public function failing_to_retrieve_metadata(Exception $exception, string $getterName): void |
||
189 | { |
||
190 | $adapter = $this->adapter(); |
||
191 | $result = new Result([ |
||
192 | 'Key' => static::$adapterPrefix . '/filename.txt', |
||
193 | ]); |
||
194 | static::$stubS3Client->stageResultForCommand('HeadObject', $result); |
||
195 | |||
196 | $this->expectExceptionObject($exception); |
||
197 | |||
198 | $adapter->{$getterName}('filename.txt'); |
||
199 | } |
||
200 | |||
201 | public function dpFailingMetadataGetters(): iterable |
||
202 | { |
||
203 | yield "mimeType" => [UnableToRetrieveMetadata::mimeType('filename.txt'), 'mimeType']; |
||
204 | yield "lastModified" => [UnableToRetrieveMetadata::lastModified('filename.txt'), 'lastModified']; |
||
205 | yield "fileSize" => [UnableToRetrieveMetadata::fileSize('filename.txt'), 'fileSize']; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * @test |
||
210 | */ |
||
211 | public function failing_to_check_for_file_existence(): void |
||
212 | { |
||
213 | $adapter = $this->adapter(); |
||
214 | |||
215 | static::$stubS3Client->throw500ExceptionWhenExecutingCommand('HeadObject'); |
||
216 | |||
217 | $this->expectException(UnableToCheckFileExistence::class); |
||
218 | |||
219 | $adapter->fileExists('something-that-does-exist.txt'); |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * @test |
||
224 | * @dataProvider casesWhereHttpStreamingInfluencesSeekability |
||
225 | */ |
||
226 | public function streaming_reads_are_not_seekable_and_non_streaming_are(bool $streaming, bool $seekable): void |
||
227 | { |
||
228 | if (getenv('COMPOSER_OPTS') === '--prefer-lowest') { |
||
229 | $this->markTestSkipped('The SDK does not support streaming in low versions.'); |
||
230 | } |
||
231 | |||
232 | $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming)); |
||
233 | $this->givenWeHaveAnExistingFile('path.txt'); |
||
234 | |||
235 | $resource = $adapter->readStream('path.txt'); |
||
236 | $metadata = stream_get_meta_data($resource); |
||
237 | fclose($resource); |
||
238 | |||
239 | $this->assertEquals($seekable, $metadata['seekable']); |
||
240 | } |
||
241 | |||
242 | public function casesWhereHttpStreamingInfluencesSeekability(): Generator |
||
243 | { |
||
244 | yield "not streaming reads have seekable stream" => [false, true]; |
||
245 | yield "streaming reads have non-seekable stream" => [true, false]; |
||
246 | } |
||
247 | |||
248 | /** |
||
249 | * @test |
||
250 | * @dataProvider casesWhereHttpStreamingInfluencesSeekability |
||
251 | */ |
||
252 | public function configuring_http_streaming_via_options(bool $streaming): void |
||
253 | { |
||
254 | $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming, ['@http' => ['stream' => false]])); |
||
255 | $this->givenWeHaveAnExistingFile('path.txt'); |
||
256 | |||
257 | $resource = $adapter->readStream('path.txt'); |
||
258 | $metadata = stream_get_meta_data($resource); |
||
259 | fclose($resource); |
||
260 | |||
261 | $this->assertTrue($metadata['seekable']); |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * @test |
||
266 | */ |
||
267 | public function moving_with_updated_metadata(): void |
||
268 | { |
||
269 | $adapter = $this->adapter(); |
||
270 | $adapter->write('source.txt', 'contents to be moved', new Config(['ContentType' => 'text/plain'])); |
||
271 | $mimeTypeSource = $adapter->mimeType('source.txt')->mimeType(); |
||
272 | $this->assertSame('text/plain', $mimeTypeSource); |
||
273 | |||
274 | $adapter->move('source.txt', 'destination.txt', new Config( |
||
275 | ['ContentType' => 'text/plain+special', 'MetadataDirective' => 'REPLACE'] |
||
276 | )); |
||
277 | $mimeTypeDestination = $adapter->mimeType('destination.txt')->mimeType(); |
||
278 | $this->assertSame('text/plain+special', $mimeTypeDestination); |
||
279 | } |
||
280 | |||
281 | protected static function createFilesystemAdapter(bool $streaming = true, array $options = []): FilesystemAdapter |
||
282 | { |
||
283 | static::$stubS3Client = new S3ClientStub(static::s3Client()); |
||
284 | /** @var string $bucket */ |
||
285 | $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET'); |
||
286 | $prefix = getenv('FLYSYSTEM_AWS_S3_PREFIX') ?: static::$adapterPrefix; |
||
287 | |||
288 | return new AwsS3V3Adapter(static::$stubS3Client, $bucket, $prefix, null, null, $options, $streaming); |
||
0 ignored issues
–
show
Since
$stubS3Client is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self , or increasing the visibility of $stubS3Client to at least protected.
Let’s assume you have a class which uses late-static binding: class YourClass
{
private static $someVariable;
public static function getSomeVariable()
{
return static::$someVariable;
}
}
The code above will run fine in your PHP runtime. However, if you now create a
sub-class and call the class YourSubClass extends YourClass { }
YourSubClass::getSomeVariable(); // Will cause an access error.
In the case above, it makes sense to update class SomeClass
{
private static $someVariable;
public static function getSomeVariable()
{
return self::$someVariable; // self works fine with private.
}
}
![]() |
|||
289 | } |
||
290 | } |
||
291 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.