Completed
Push — 2.x ( 3ea09a...0ff08f )
by Frank
01:08
created

AwsS3V3AdapterTest.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

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
22
/**
23
 * @group aws
24
 */
25
class AwsS3V3AdapterTest extends FilesystemAdapterTestCase
26
{
27
    /**
28
     * @var bool
29
     */
30
    private $shouldCleanUp = false;
31
32
    /**
33
     * @var string
34
     */
35
    private static $adapterPrefix = 'test-prefix';
36
37
    /**
38
     * @var S3ClientInterface|null
39
     */
40
    private static $s3Client;
41
42
    /**
43
     * @var S3ClientStub
44
     */
45
    private static $stubS3Client;
46
47
    public static function setUpBeforeClass(): void
48
    {
49
        static::$adapterPrefix = 'ci/' . bin2hex(random_bytes(10));
50
    }
51
52
    protected function tearDown(): void
53
    {
54
        if ( ! $this->shouldCleanUp) {
55
            return;
56
        }
57
58
        $adapter = $this->adapter();
59
        $adapter->deleteDirectory('/');
60
        /** @var StorageAttributes[] $listing */
61
        $listing = $adapter->listContents('', false);
62
63
        foreach ($listing as $item) {
64
            if ($item->isFile()) {
65
                $adapter->delete($item->path());
66
            } else {
67
                $adapter->deleteDirectory($item->path());
68
            }
69
        }
70
    }
71
72
    private static function s3Client(): S3ClientInterface
73
    {
74
        if (static::$s3Client instanceof S3ClientInterface) {
75
            return static::$s3Client;
76
        }
77
78
        $key = getenv('FLYSYSTEM_AWS_S3_KEY');
79
        $secret = getenv('FLYSYSTEM_AWS_S3_SECRET');
80
        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');
81
        $region = getenv('FLYSYSTEM_AWS_S3_REGION') ?: 'eu-central-1';
82
83
        if ( ! $key || ! $secret || ! $bucket) {
84
            self::markTestSkipped('No AWS credentials present for testing.');
85
        }
86
87
        $options = ['version' => 'latest', 'credentials' => compact('key', 'secret'), 'region' => $region];
88
89
        return static::$s3Client = new S3Client($options);
90
    }
91
92
    /**
93
     * @test
94
     */
95
    public function writing_with_a_specific_mime_type(): void
96
    {
97
        $adapter = $this->adapter();
98
        $adapter->write('some/path.txt', 'contents', new Config(['ContentType' => 'text/plain+special']));
99
        $mimeType = $adapter->mimeType('some/path.txt')->mimeType();
100
        $this->assertEquals('text/plain+special', $mimeType);
101
    }
102
103
    /**
104
     * @test
105
     */
106
    public function listing_contents_recursive(): void
107
    {
108
        $adapter = $this->adapter();
109
        $adapter->write('something/0/here.txt', 'contents', new Config());
110
        $adapter->write('something/1/also/here.txt', 'contents', new Config());
111
112
        $contents = iterator_to_array($adapter->listContents('', true));
113
114
        $this->assertCount(2, $contents);
115
        $this->assertContainsOnlyInstancesOf(FileAttributes::class, $contents);
116
        /** @var FileAttributes $file */
117
        $file = $contents[0];
118
        $this->assertEquals('something/0/here.txt', $file->path());
119
        /** @var FileAttributes $file */
120
        $file = $contents[1];
121
        $this->assertEquals('something/1/also/here.txt', $file->path());
122
    }
123
124
    /**
125
     * @test
126
     */
127
    public function failing_to_delete_while_moving(): void
128
    {
129
        $adapter = $this->adapter();
130
        $adapter->write('source.txt', 'contents to be copied', new Config());
131
        static::$stubS3Client->failOnNextCopy();
132
133
        $this->expectException(UnableToMoveFile::class);
134
135
        $adapter->move('source.txt', 'destination.txt', new Config());
136
    }
137
138
    /**
139
     * @test
140
     */
141
    public function failing_to_delete_a_file(): void
142
    {
143
        $adapter = $this->adapter();
144
        static::$stubS3Client->throwExceptionWhenExecutingCommand('DeleteObject');
145
146
        $this->expectException(UnableToDeleteFile::class);
147
148
        $adapter->delete('path.txt');
149
    }
150
151
    /**
152
     * @test
153
     */
154
    public function fetching_unknown_mime_type_of_a_file(): void
155
    {
156
        $this->adapter();
157
        $result = new Result([
158
            'Key' => static::$adapterPrefix . '/unknown-mime-type.md5',
159
        ]);
160
        static::$stubS3Client->stageResultForCommand('HeadObject', $result);
161
162
        parent::fetching_unknown_mime_type_of_a_file();
163
    }
164
165
    /**
166
     * @test
167
     * @dataProvider dpFailingMetadataGetters
168
     */
169
    public function failing_to_retrieve_metadata(Exception $exception, string $getterName): void
170
    {
171
        $adapter = $this->adapter();
172
        $result = new Result([
173
             'Key' => static::$adapterPrefix . '/filename.txt',
0 ignored issues
show
Since $adapterPrefix is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $adapterPrefix 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 getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
174
        ]);
175
        static::$stubS3Client->stageResultForCommand('HeadObject', $result);
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 getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
176
177
        $this->expectExceptionObject($exception);
178
179
        $adapter->{$getterName}('filename.txt');
180
    }
181
182
    public function dpFailingMetadataGetters(): iterable
183
    {
184
        yield "mimeType" => [UnableToRetrieveMetadata::mimeType('filename.txt'), 'mimeType'];
185
        yield "lastModified" => [UnableToRetrieveMetadata::lastModified('filename.txt'), 'lastModified'];
186
        yield "fileSize" => [UnableToRetrieveMetadata::fileSize('filename.txt'), 'fileSize'];
187
    }
188
189
    /**
190
     * @test
191
     */
192
    public function failing_to_check_for_file_existence(): void
193
    {
194
        $adapter = $this->adapter();
195
196
        static::$stubS3Client->throw500ExceptionWhenExecutingCommand('HeadObject');
197
198
        $this->expectException(UnableToCheckFileExistence::class);
199
200
        $adapter->fileExists('something-that-does-exist.txt');
201
    }
202
203
    /**
204
     * @test
205
     * @dataProvider casesWhereHttpStreamingInfluencesSeekability
206
     */
207
    public function streaming_reads_are_not_seekable_and_non_streaming_are(bool $streaming, bool $seekable): void
208
    {
209
        if (getenv('COMPOSER_OPTS') === '--prefer-lowest') {
210
            $this->markTestSkipped('The SDK does not support streaming in low versions.');
211
        }
212
213
        $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming));
214
        $this->givenWeHaveAnExistingFile('path.txt');
215
216
        $resource = $adapter->readStream('path.txt');
217
        $metadata = stream_get_meta_data($resource);
218
        fclose($resource);
219
220
        $this->assertEquals($seekable, $metadata['seekable']);
221
    }
222
223
    public function casesWhereHttpStreamingInfluencesSeekability(): Generator
224
    {
225
        yield "not streaming reads have seekable stream" => [false, true];
226
        yield "streaming reads have non-seekable stream" => [true, false];
227
    }
228
229
    /**
230
     * @test
231
     * @dataProvider casesWhereHttpStreamingInfluencesSeekability
232
     */
233
    public function configuring_http_streaming_via_options(bool $streaming): void
234
    {
235
        $adapter = $this->useAdapter($this->createFilesystemAdapter($streaming, ['@http' => ['stream' => false]]));
236
        $this->givenWeHaveAnExistingFile('path.txt');
237
238
        $resource = $adapter->readStream('path.txt');
239
        $metadata = stream_get_meta_data($resource);
240
        fclose($resource);
241
242
        $this->assertTrue($metadata['seekable']);
243
    }
244
245
    protected static function createFilesystemAdapter(bool $streaming = true, array $options = []): FilesystemAdapter
246
    {
247
        static::$stubS3Client = new S3ClientStub(static::s3Client());
248
        /** @var string $bucket */
249
        $bucket = getenv('FLYSYSTEM_AWS_S3_BUCKET');
250
        $prefix = getenv('FLYSYSTEM_AWS_S3_PREFIX') ?: static::$adapterPrefix;
251
252
        return new AwsS3V3Adapter(static::$stubS3Client, $bucket, $prefix, null, null, $options, $streaming);
253
    }
254
}
255