middleman-s3_sync 4.6.1 → 4.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.envrc.backup +5 -0
- data/.gitignore +1 -0
- data/.mise.toml +17 -0
- data/Changelog.md +12 -0
- data/WARP.md +112 -0
- data/lib/middleman/s3_sync/options.rb +26 -3
- data/lib/middleman/s3_sync/resource.rb +59 -39
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +3 -3
- data/lib/middleman-s3_sync/extension.rb +22 -1
- data/middleman-s3_sync.gemspec +2 -4
- data/spec/aws_sdk_parameters_spec.rb +525 -0
- data/spec/extension_spec.rb +114 -0
- data/spec/options_spec.rb +272 -0
- metadata +17 -36
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'AWS SDK Parameter Validation' do
|
|
4
|
+
let(:options) { Middleman::S3Sync::Options.new }
|
|
5
|
+
let(:s3_client) { instance_double(Aws::S3::Client) }
|
|
6
|
+
let(:s3_resource) { instance_double(Aws::S3::Resource) }
|
|
7
|
+
let(:bucket) { instance_double(Aws::S3::Bucket) }
|
|
8
|
+
let(:s3_object) { instance_double(Aws::S3::Object) }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
Middleman::S3Sync.s3_sync_options = options
|
|
12
|
+
options.build_dir = "build"
|
|
13
|
+
options.bucket = "test-bucket"
|
|
14
|
+
options.acl = "public-read"
|
|
15
|
+
options.index_document = "index.html"
|
|
16
|
+
options.error_document = "404.html"
|
|
17
|
+
options.version_bucket = true
|
|
18
|
+
|
|
19
|
+
allow(Aws::S3::Client).to receive(:new).and_return(s3_client)
|
|
20
|
+
allow(Aws::S3::Resource).to receive(:new).and_return(s3_resource)
|
|
21
|
+
allow(s3_resource).to receive(:bucket).and_return(bucket)
|
|
22
|
+
allow(bucket).to receive(:exists?).and_return(true)
|
|
23
|
+
allow(bucket).to receive(:object).and_return(s3_object)
|
|
24
|
+
allow(s3_object).to receive(:put).and_return(true)
|
|
25
|
+
|
|
26
|
+
# Allow Middleman::S3Sync to use our mocked client/bucket
|
|
27
|
+
allow(Middleman::S3Sync).to receive(:s3_client).and_return(s3_client)
|
|
28
|
+
allow(Middleman::S3Sync).to receive(:bucket).and_return(bucket)
|
|
29
|
+
allow(Middleman::S3Sync).to receive(:say_status)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'put_bucket_website parameters' do
|
|
33
|
+
it 'uses symbol keys for index_document and error_document' do
|
|
34
|
+
expect(s3_client).to receive(:put_bucket_website) do |params|
|
|
35
|
+
expect(params[:bucket]).to eq("test-bucket")
|
|
36
|
+
expect(params[:website_configuration]).to have_key(:index_document)
|
|
37
|
+
expect(params[:website_configuration]).to have_key(:error_document)
|
|
38
|
+
|
|
39
|
+
# Verify the nested structure uses symbols, not strings
|
|
40
|
+
expect(params[:website_configuration][:index_document]).to have_key(:suffix)
|
|
41
|
+
expect(params[:website_configuration][:error_document]).to have_key(:key)
|
|
42
|
+
|
|
43
|
+
# Verify the values are correct
|
|
44
|
+
expect(params[:website_configuration][:index_document][:suffix]).to eq("index.html")
|
|
45
|
+
expect(params[:website_configuration][:error_document][:key]).to eq("404.html")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Middleman::S3Sync.send(:update_bucket_website)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'when only index_document is set' do
|
|
52
|
+
before do
|
|
53
|
+
options.error_document = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'only includes index_document in website configuration' do
|
|
57
|
+
expect(s3_client).to receive(:put_bucket_website) do |params|
|
|
58
|
+
expect(params[:website_configuration]).to have_key(:index_document)
|
|
59
|
+
expect(params[:website_configuration]).not_to have_key(:error_document)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Middleman::S3Sync.send(:update_bucket_website)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'when neither document is set' do
|
|
67
|
+
before do
|
|
68
|
+
options.index_document = nil
|
|
69
|
+
options.error_document = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'does not call put_bucket_website' do
|
|
73
|
+
expect(s3_client).not_to receive(:put_bucket_website)
|
|
74
|
+
|
|
75
|
+
Middleman::S3Sync.send(:update_bucket_website)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'when only error_document is set' do
|
|
80
|
+
before do
|
|
81
|
+
options.index_document = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'raises an error because S3 requires index_document if error_document is specified' do
|
|
85
|
+
expect {
|
|
86
|
+
Middleman::S3Sync.send(:update_bucket_website)
|
|
87
|
+
}.to raise_error('S3 requires `index_document` if `error_document` is specified')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe 'put_bucket_versioning parameters' do
|
|
93
|
+
it 'uses correct parameter structure' do
|
|
94
|
+
expect(s3_client).to receive(:put_bucket_versioning) do |params|
|
|
95
|
+
expect(params[:bucket]).to eq("test-bucket")
|
|
96
|
+
expect(params[:versioning_configuration]).to be_a(Hash)
|
|
97
|
+
expect(params[:versioning_configuration][:status]).to eq("Enabled")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
Middleman::S3Sync.send(:update_bucket_versioning)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context 'when version_bucket is false' do
|
|
104
|
+
before do
|
|
105
|
+
options.version_bucket = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'does not call put_bucket_versioning' do
|
|
109
|
+
expect(s3_client).not_to receive(:put_bucket_versioning)
|
|
110
|
+
|
|
111
|
+
Middleman::S3Sync.send(:update_bucket_versioning)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
describe 'S3 object upload parameters' do
|
|
117
|
+
let(:mm_resource) do
|
|
118
|
+
double(
|
|
119
|
+
destination_path: 'test/file.html',
|
|
120
|
+
content_type: 'text/html'
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
125
|
+
|
|
126
|
+
before do
|
|
127
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
128
|
+
allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
|
|
129
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
|
|
130
|
+
allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
|
|
131
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
132
|
+
options.dry_run = false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'uses correct metadata key format' do
|
|
136
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
137
|
+
# Verify basic parameters
|
|
138
|
+
expect(upload_options[:body]).to eq('test content')
|
|
139
|
+
expect(upload_options[:content_type]).to eq('text/html')
|
|
140
|
+
expect(upload_options[:acl]).to eq('public-read')
|
|
141
|
+
|
|
142
|
+
# Verify metadata uses correct key format (suffix only, not full header name)
|
|
143
|
+
expect(upload_options[:metadata]).to be_a(Hash)
|
|
144
|
+
expect(upload_options[:metadata]).to have_key('content-md5')
|
|
145
|
+
expect(upload_options[:metadata]).not_to have_key('x-amz-meta-content-md5')
|
|
146
|
+
|
|
147
|
+
# Verify metadata value is the MD5 hash
|
|
148
|
+
expected_md5 = Digest::MD5.hexdigest('test content')
|
|
149
|
+
expect(upload_options[:metadata]['content-md5']).to eq(expected_md5)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
resource.upload!
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context 'when ACL is set to empty string (for buckets with ACLs disabled)' do
|
|
156
|
+
before do
|
|
157
|
+
options.acl = ''
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'does not include acl parameter in upload' do
|
|
161
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
162
|
+
expect(upload_options).not_to have_key(:acl)
|
|
163
|
+
expect(upload_options[:body]).to eq('test content')
|
|
164
|
+
expect(upload_options[:content_type]).to eq('text/html')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
resource.upload!
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context 'when ACL is set to nil (for buckets with ACLs disabled)' do
|
|
172
|
+
before do
|
|
173
|
+
options.acl = nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'does not include acl parameter in upload' do
|
|
177
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
178
|
+
expect(upload_options).not_to have_key(:acl)
|
|
179
|
+
expect(upload_options[:body]).to eq('test content')
|
|
180
|
+
expect(upload_options[:content_type]).to eq('text/html')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
resource.upload!
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
context 'when bucket does not support ACLs (auto-detection)' do
|
|
188
|
+
before do
|
|
189
|
+
# ACL is enabled by default
|
|
190
|
+
expect(options.acl).to eq('public-read')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'automatically retries without ACL when AccessControlListNotSupported error occurs' do
|
|
194
|
+
call_count = 0
|
|
195
|
+
expect(s3_object).to receive(:put).twice do |upload_options|
|
|
196
|
+
call_count += 1
|
|
197
|
+
if call_count == 1
|
|
198
|
+
# First call should include ACL
|
|
199
|
+
expect(upload_options[:acl]).to eq('public-read')
|
|
200
|
+
raise Aws::S3::Errors::AccessControlListNotSupported.new(nil, 'The bucket does not allow ACLs')
|
|
201
|
+
else
|
|
202
|
+
# Second call should not include ACL
|
|
203
|
+
expect(upload_options).not_to have_key(:acl)
|
|
204
|
+
expect(upload_options[:body]).to eq('test content')
|
|
205
|
+
expect(upload_options[:content_type]).to eq('text/html')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Should automatically disable ACLs after the error
|
|
210
|
+
resource.upload!
|
|
211
|
+
expect(options.acl_enabled?).to be false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it 'permanently disables ACLs after detecting bucket does not support them' do
|
|
215
|
+
call_count = 0
|
|
216
|
+
allow(s3_object).to receive(:put) do |upload_options|
|
|
217
|
+
call_count += 1
|
|
218
|
+
if call_count == 1
|
|
219
|
+
expect(upload_options[:acl]).to eq('public-read')
|
|
220
|
+
raise Aws::S3::Errors::AccessControlListNotSupported.new(nil, 'The bucket does not allow ACLs')
|
|
221
|
+
else
|
|
222
|
+
# Second call should succeed without ACL
|
|
223
|
+
expect(upload_options).not_to have_key(:acl)
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
resource.upload!
|
|
229
|
+
expect(options.acl_enabled?).to be false
|
|
230
|
+
|
|
231
|
+
# Verify ACLs stay disabled for future uploads
|
|
232
|
+
resource.upload!
|
|
233
|
+
expect(call_count).to eq(3) # First attempt, retry, and third upload
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
context 'when gzip is enabled' do
|
|
238
|
+
before do
|
|
239
|
+
options.prefer_gzip = true
|
|
240
|
+
allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(true)
|
|
241
|
+
allow(File).to receive(:read).with('build/test/file.html.gz').and_return('gzipped content')
|
|
242
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
243
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('original content')
|
|
244
|
+
|
|
245
|
+
# Mock the HEAD response to avoid calling it during redirect?
|
|
246
|
+
head_response = double(
|
|
247
|
+
metadata: {},
|
|
248
|
+
etag: '"abc123"',
|
|
249
|
+
content_encoding: nil,
|
|
250
|
+
cache_control: nil,
|
|
251
|
+
website_redirect_location: nil
|
|
252
|
+
)
|
|
253
|
+
allow(s3_object).to receive(:head).and_return(head_response)
|
|
254
|
+
resource.instance_variable_set(:@full_s3_resource, head_response)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'includes content_encoding parameter' do
|
|
258
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
259
|
+
expect(upload_options[:content_encoding]).to eq('gzip')
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
resource.upload!
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
context 'when reduced redundancy storage is enabled' do
|
|
267
|
+
before do
|
|
268
|
+
options.reduced_redundancy_storage = true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'includes storage_class parameter' do
|
|
272
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
273
|
+
expect(upload_options[:storage_class]).to eq('REDUCED_REDUNDANCY')
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
resource.upload!
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
context 'when encryption is enabled' do
|
|
281
|
+
before do
|
|
282
|
+
options.encryption = true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'includes server_side_encryption parameter' do
|
|
286
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
287
|
+
expect(upload_options[:server_side_encryption]).to eq('AES256')
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
resource.upload!
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
context 'when resource has a redirect' do
|
|
295
|
+
let(:mm_resource) do
|
|
296
|
+
double(
|
|
297
|
+
destination_path: 'redirect/file.html',
|
|
298
|
+
content_type: 'text/html',
|
|
299
|
+
redirect?: true,
|
|
300
|
+
target_url: 'https://example.com/new-location'
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
before do
|
|
305
|
+
allow(File).to receive(:exist?).with('build/redirect/file.html').and_return(true)
|
|
306
|
+
allow(File).to receive(:exist?).with('build/redirect/file.html.gz').and_return(false)
|
|
307
|
+
allow(File).to receive(:read).with('build/redirect/file.html').and_return('redirect content')
|
|
308
|
+
allow(File).to receive(:directory?).with('build/redirect/file.html').and_return(false)
|
|
309
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
310
|
+
allow(resource).to receive(:redirect?).and_return(true)
|
|
311
|
+
allow(resource).to receive(:redirect_url).and_return('https://example.com/new-location')
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'includes website_redirect_location parameter' do
|
|
315
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
316
|
+
expect(upload_options[:website_redirect_location]).to eq('https://example.com/new-location')
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
resource.upload!
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
describe 'S3 object metadata retrieval' do
|
|
325
|
+
let(:mm_resource) do
|
|
326
|
+
double(
|
|
327
|
+
destination_path: 'test/file.html',
|
|
328
|
+
content_type: 'text/html'
|
|
329
|
+
)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
333
|
+
let(:head_response) do
|
|
334
|
+
double(
|
|
335
|
+
metadata: { 'content-md5' => 'abc123def456' },
|
|
336
|
+
etag: '"def456abc123"',
|
|
337
|
+
content_encoding: nil,
|
|
338
|
+
cache_control: nil,
|
|
339
|
+
website_redirect_location: nil
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
before do
|
|
344
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
345
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
|
|
346
|
+
allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
|
|
347
|
+
allow(s3_object).to receive(:head).and_return(head_response)
|
|
348
|
+
resource.instance_variable_set(:@full_s3_resource, head_response)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'reads metadata using correct key format' do
|
|
352
|
+
expect(resource.remote_content_md5).to eq('abc123def456')
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'does not try to read metadata with old header format' do
|
|
356
|
+
# Ensure it's not looking for the full header name
|
|
357
|
+
expect(head_response.metadata).not_to receive(:[]).with('x-amz-meta-content-md5')
|
|
358
|
+
|
|
359
|
+
resource.remote_content_md5
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
describe 'to_h method for legacy compatibility' do
|
|
364
|
+
let(:mm_resource) do
|
|
365
|
+
double(
|
|
366
|
+
destination_path: 'test/file.html',
|
|
367
|
+
content_type: 'text/html'
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
372
|
+
|
|
373
|
+
before do
|
|
374
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
375
|
+
allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
|
|
376
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
|
|
377
|
+
allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
|
|
378
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it 'returns attributes with correct key formats' do
|
|
382
|
+
attributes = resource.to_h
|
|
383
|
+
|
|
384
|
+
expect(attributes[:key]).to eq('test/file.html')
|
|
385
|
+
expect(attributes[:acl]).to eq('public-read')
|
|
386
|
+
expect(attributes[:content_type]).to eq('text/html')
|
|
387
|
+
expect(attributes['content-md5']).to be_a(String)
|
|
388
|
+
expect(attributes).not_to have_key('x-amz-meta-content-md5')
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
context 'when ACL is set to empty string' do
|
|
392
|
+
before do
|
|
393
|
+
options.acl = ''
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'does not include acl in attributes' do
|
|
397
|
+
attributes = resource.to_h
|
|
398
|
+
|
|
399
|
+
expect(attributes).not_to have_key(:acl)
|
|
400
|
+
expect(attributes[:key]).to eq('test/file.html')
|
|
401
|
+
expect(attributes[:content_type]).to eq('text/html')
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
context 'when ACL is set to nil' do
|
|
406
|
+
before do
|
|
407
|
+
options.acl = nil
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
it 'does not include acl in attributes' do
|
|
411
|
+
attributes = resource.to_h
|
|
412
|
+
|
|
413
|
+
expect(attributes).not_to have_key(:acl)
|
|
414
|
+
expect(attributes[:key]).to eq('test/file.html')
|
|
415
|
+
expect(attributes[:content_type]).to eq('text/html')
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
context 'when resource has a redirect' do
|
|
420
|
+
let(:mm_resource) do
|
|
421
|
+
double(
|
|
422
|
+
destination_path: 'redirect/file.html',
|
|
423
|
+
content_type: 'text/html',
|
|
424
|
+
redirect?: true,
|
|
425
|
+
target_url: 'https://example.com/new-location'
|
|
426
|
+
)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
before do
|
|
430
|
+
allow(File).to receive(:exist?).with('build/redirect/file.html').and_return(true)
|
|
431
|
+
allow(File).to receive(:exist?).with('build/redirect/file.html.gz').and_return(false)
|
|
432
|
+
allow(File).to receive(:read).with('build/redirect/file.html').and_return('redirect content')
|
|
433
|
+
allow(File).to receive(:directory?).with('build/redirect/file.html').and_return(false)
|
|
434
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
435
|
+
allow(resource).to receive(:redirect?).and_return(true)
|
|
436
|
+
allow(resource).to receive(:redirect_url).and_return('https://example.com/new-location')
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it 'includes redirect with correct key format' do
|
|
440
|
+
attributes = resource.to_h
|
|
441
|
+
|
|
442
|
+
expect(attributes['website-redirect-location']).to eq('https://example.com/new-location')
|
|
443
|
+
expect(attributes).not_to have_key('x-amz-website-redirect-location')
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
describe 'Regression tests for Fog-style parameter issues' do
|
|
449
|
+
# These tests validate that we've fixed the old Fog-style parameter formatting
|
|
450
|
+
# and demonstrate what would fail if we reverted to the old style
|
|
451
|
+
|
|
452
|
+
it 'does not use string keys for website configuration (old Fog style)' do
|
|
453
|
+
# This would fail if we reverted to the old format:
|
|
454
|
+
# opts[:index_document] = { "suffix" => s3_sync_options.index_document }
|
|
455
|
+
|
|
456
|
+
expect(s3_client).to receive(:put_bucket_website) do |params|
|
|
457
|
+
config = params[:website_configuration]
|
|
458
|
+
|
|
459
|
+
# Ensure we're not using string keys (old Fog style)
|
|
460
|
+
expect(config[:index_document]).not_to have_key("suffix")
|
|
461
|
+
expect(config[:error_document]).not_to have_key("key")
|
|
462
|
+
|
|
463
|
+
# Ensure we ARE using symbol keys (correct AWS SDK style)
|
|
464
|
+
expect(config[:index_document]).to have_key(:suffix)
|
|
465
|
+
expect(config[:error_document]).to have_key(:key)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
Middleman::S3Sync.send(:update_bucket_website)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
it 'does not use full header names in metadata (old style)' do
|
|
472
|
+
mm_resource = double(
|
|
473
|
+
destination_path: 'test/file.html',
|
|
474
|
+
content_type: 'text/html'
|
|
475
|
+
)
|
|
476
|
+
resource = Middleman::S3Sync::Resource.new(mm_resource, nil)
|
|
477
|
+
|
|
478
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
479
|
+
allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
|
|
480
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
|
|
481
|
+
allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
|
|
482
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
483
|
+
options.dry_run = false
|
|
484
|
+
|
|
485
|
+
expect(s3_object).to receive(:put) do |upload_options|
|
|
486
|
+
# Ensure we're not using the old full header format
|
|
487
|
+
expect(upload_options[:metadata]).not_to have_key('x-amz-meta-content-md5')
|
|
488
|
+
|
|
489
|
+
# Ensure we ARE using the correct suffix-only format
|
|
490
|
+
expect(upload_options[:metadata]).to have_key('content-md5')
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
resource.upload!
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it 'validates that old constants are no longer used' do
|
|
497
|
+
# This test ensures the old constants were removed/changed
|
|
498
|
+
# If they still existed, this would be a sign we didn't clean up properly
|
|
499
|
+
|
|
500
|
+
mm_resource = double(
|
|
501
|
+
destination_path: 'test/file.html',
|
|
502
|
+
content_type: 'text/html'
|
|
503
|
+
)
|
|
504
|
+
resource = Middleman::S3Sync::Resource.new(mm_resource, nil)
|
|
505
|
+
|
|
506
|
+
allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
|
|
507
|
+
allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
|
|
508
|
+
allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
|
|
509
|
+
allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
|
|
510
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
|
511
|
+
allow(resource).to receive(:redirect?).and_return(true)
|
|
512
|
+
allow(resource).to receive(:redirect_url).and_return('https://example.com/redirect')
|
|
513
|
+
|
|
514
|
+
attributes = resource.to_h
|
|
515
|
+
|
|
516
|
+
# Validate that the old constant values are not used
|
|
517
|
+
expect(attributes).not_to have_key('x-amz-meta-content-md5')
|
|
518
|
+
expect(attributes).not_to have_key('x-amz-website-redirect-location')
|
|
519
|
+
|
|
520
|
+
# Validate that the correct formats are used
|
|
521
|
+
expect(attributes).to have_key('content-md5')
|
|
522
|
+
expect(attributes).to have_key('website-redirect-location')
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Middleman::S3SyncExtension do
|
|
4
|
+
let(:app) { double('app').as_null_object }
|
|
5
|
+
|
|
6
|
+
let(:extension) { described_class.new(app, {}) }
|
|
7
|
+
|
|
8
|
+
describe '#acl_enabled?' do
|
|
9
|
+
context 'when acl has default value' do
|
|
10
|
+
it 'returns true' do
|
|
11
|
+
expect(extension.acl_enabled?).to be true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context 'when acl is explicitly set to a value' do
|
|
16
|
+
let(:extension) { described_class.new(app, acl: 'private') }
|
|
17
|
+
|
|
18
|
+
it 'returns true' do
|
|
19
|
+
expect(extension.acl_enabled?).to be true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'when acl is set to nil' do
|
|
24
|
+
let(:extension) { described_class.new(app, acl: nil) }
|
|
25
|
+
|
|
26
|
+
it 'returns false' do
|
|
27
|
+
expect(extension.acl_enabled?).to be false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'when acl is set to empty string' do
|
|
32
|
+
let(:extension) { described_class.new(app, acl: '') }
|
|
33
|
+
|
|
34
|
+
it 'returns false' do
|
|
35
|
+
expect(extension.acl_enabled?).to be false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'when acl is set to false' do
|
|
40
|
+
let(:extension) { described_class.new(app, acl: false) }
|
|
41
|
+
|
|
42
|
+
it 'returns false' do
|
|
43
|
+
expect(extension.acl_enabled?).to be false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '#s3_sync_options' do
|
|
49
|
+
it 'returns the extension instance itself' do
|
|
50
|
+
expect(extension.s3_sync_options).to eq(extension)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'allows access to acl_enabled? through s3_sync_options' do
|
|
54
|
+
expect(extension.s3_sync_options.acl_enabled?).to be true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe 'option delegation' do
|
|
59
|
+
let(:extension) do
|
|
60
|
+
described_class.new(app,
|
|
61
|
+
bucket: 'test-bucket',
|
|
62
|
+
region: 'us-west-2',
|
|
63
|
+
acl: 'public-read',
|
|
64
|
+
verbose: true
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'delegates option readers to the options object' do
|
|
69
|
+
expect(extension.bucket).to eq('test-bucket')
|
|
70
|
+
expect(extension.region).to eq('us-west-2')
|
|
71
|
+
expect(extension.acl).to eq('public-read')
|
|
72
|
+
expect(extension.verbose).to be true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'responds to option methods' do
|
|
76
|
+
expect(extension.respond_to?(:bucket)).to be true
|
|
77
|
+
expect(extension.respond_to?(:region)).to be true
|
|
78
|
+
expect(extension.respond_to?(:acl)).to be true
|
|
79
|
+
expect(extension.respond_to?(:verbose)).to be true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'does not respond to non-existent methods' do
|
|
83
|
+
expect(extension.respond_to?(:nonexistent_method)).to be false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'raises NoMethodError for non-existent methods' do
|
|
87
|
+
expect { extension.nonexistent_method }.to raise_error(NoMethodError)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe 'integration with S3Sync module' do
|
|
92
|
+
before do
|
|
93
|
+
allow(Middleman::Application).to receive(:root).and_return('/tmp')
|
|
94
|
+
allow(File).to receive(:exist?).with('/tmp/.s3_sync').and_return(false)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
let(:extension) do
|
|
98
|
+
described_class.new(app,
|
|
99
|
+
bucket: 'test-bucket',
|
|
100
|
+
acl: 'private'
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'sets s3_sync_options to the extension instance' do
|
|
105
|
+
extension.after_configuration
|
|
106
|
+
expect(Middleman::S3Sync.s3_sync_options).to eq(extension)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'allows S3Sync to access acl_enabled? through s3_sync_options' do
|
|
110
|
+
extension.after_configuration
|
|
111
|
+
expect(Middleman::S3Sync.s3_sync_options.acl_enabled?).to be true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|