middleman-s3_sync 4.6.5 → 4.8.0

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.
@@ -15,11 +15,21 @@ describe 'S3Sync CloudFront Integration' do
15
15
  dry_run: false,
16
16
  verbose: false,
17
17
  delete: true,
18
- bucket: 'test-bucket'
18
+ bucket: 'test-bucket',
19
+ after_s3_sync: nil
19
20
  )
20
21
  end
21
22
 
22
23
  before do
24
+ # Reset cached app to avoid double leakage between tests
25
+ Middleman::S3Sync.instance_variable_set(:@app, nil)
26
+
27
+ # Mock sitemap for ensure_resource_list_updated! call
28
+ sitemap = double('sitemap')
29
+ allow(sitemap).to receive(:ensure_resource_list_updated!)
30
+ allow(app).to receive(:respond_to?).with(:sitemap).and_return(true)
31
+ allow(app).to receive(:sitemap).and_return(sitemap)
32
+
23
33
  allow(::Middleman::Application).to receive(:new).and_return(app)
24
34
  allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(s3_sync_options)
25
35
  allow(Middleman::S3Sync).to receive(:say_status)
@@ -70,7 +80,8 @@ describe 'S3Sync CloudFront Integration' do
70
80
  cloudfront_invalidate: true,
71
81
  cloudfront_distribution_id: 'E1234567890123',
72
82
  cloudfront_invalidate_all: true,
73
- bucket: 'test-bucket'
83
+ bucket: 'test-bucket',
84
+ after_s3_sync: nil
74
85
  )
75
86
  end
76
87
 
@@ -89,7 +100,8 @@ describe 'S3Sync CloudFront Integration' do
89
100
  let(:s3_sync_options) do
90
101
  double(
91
102
  cloudfront_invalidate: false,
92
- bucket: 'test-bucket'
103
+ bucket: 'test-bucket',
104
+ after_s3_sync: nil
93
105
  )
94
106
  end
95
107
 
@@ -101,6 +113,28 @@ describe 'S3Sync CloudFront Integration' do
101
113
  end
102
114
  end
103
115
 
116
+ describe 'sitemap population' do
117
+ it 'calls ensure_resource_list_updated! before processing resources' do
118
+ sitemap = double('sitemap')
119
+ expect(sitemap).to receive(:ensure_resource_list_updated!)
120
+ allow(sitemap).to receive(:respond_to?).with(:ensure_resource_list_updated!).and_return(true)
121
+ allow(app).to receive(:sitemap).and_return(sitemap)
122
+
123
+ Middleman::S3Sync.sync
124
+ end
125
+
126
+ it 'handles apps without sitemap gracefully' do
127
+ Middleman::S3Sync.instance_variable_set(:@app, nil)
128
+
129
+ app_without_sitemap = double('app_without_sitemap')
130
+ allow(app_without_sitemap).to receive(:respond_to?).with(:sitemap).and_return(false)
131
+ allow(::Middleman::Application).to receive(:new).and_return(app_without_sitemap)
132
+
133
+ # Should not raise an error
134
+ expect { Middleman::S3Sync.sync }.not_to raise_error
135
+ end
136
+ end
137
+
104
138
  describe 'path tracking during resource operations' do
105
139
  before do
106
140
  # Reset invalidation paths before each path tracking test
@@ -131,6 +165,128 @@ describe 'S3Sync CloudFront Integration' do
131
165
  end
132
166
  end
133
167
 
168
+ describe 'orphan file discovery (scan_build_dir)' do
169
+ let(:build_dir) { Dir.mktmpdir }
170
+
171
+ after do
172
+ FileUtils.remove_entry(build_dir) if File.directory?(build_dir)
173
+ end
174
+
175
+ before do
176
+ # Reset state
177
+ Middleman::S3Sync.instance_variable_set(:@s3_sync_resources, {})
178
+ Middleman::S3Sync.instance_variable_set(:@bucket_files, {})
179
+
180
+ allow(Middleman::S3Sync).to receive(:say_status)
181
+ allow(Middleman::S3Sync).to receive(:build_dir).and_return(build_dir)
182
+ allow(Middleman::S3Sync).to receive(:remote_resource_for_path).and_return(nil)
183
+ end
184
+
185
+ context 'when scan_build_dir is enabled' do
186
+ let(:s3_sync_options) do
187
+ double(
188
+ scan_build_dir: true,
189
+ build_dir: build_dir,
190
+ bucket: 'test-bucket',
191
+ prefix: nil,
192
+ delete: false,
193
+ verbose: false,
194
+ ignore_paths: [],
195
+ prefer_gzip: false,
196
+ force: false,
197
+ acl: 'public-read',
198
+ after_s3_sync: nil
199
+ )
200
+ end
201
+
202
+ it 'discovers files not in sitemap' do
203
+ # Create orphan files in build directory
204
+ FileUtils.mkdir_p(File.join(build_dir, 'images'))
205
+ File.write(File.join(build_dir, 'orphan.txt'), 'orphan content')
206
+ File.write(File.join(build_dir, 'images', 'optimized.webp'), 'image data')
207
+
208
+ # Mock Resource creation to avoid S3 calls
209
+ allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
210
+ mock_resource = double('resource', status: :new, path: path)
211
+ allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
212
+ mock_resource
213
+ end
214
+
215
+ Middleman::S3Sync.send(:discover_orphan_files)
216
+
217
+ resources = Middleman::S3Sync.send(:s3_sync_resources)
218
+ expect(resources.keys).to include('orphan.txt')
219
+ expect(resources.keys).to include('images/optimized.webp')
220
+ end
221
+
222
+ it 'skips files already in sitemap' do
223
+ File.write(File.join(build_dir, 'existing.html'), 'content')
224
+
225
+ # Pre-populate sitemap resource
226
+ Middleman::S3Sync.send(:s3_sync_resources)['existing.html'] = double('resource')
227
+
228
+ # Count how many times Resource.new is called
229
+ call_count = 0
230
+ allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
231
+ call_count += 1
232
+ mock_resource = double('resource', status: :new, path: path)
233
+ allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
234
+ mock_resource
235
+ end
236
+
237
+ Middleman::S3Sync.send(:discover_orphan_files)
238
+
239
+ # Should not have created a new resource for existing.html
240
+ expect(call_count).to eq(0)
241
+ end
242
+
243
+ it 'skips directories' do
244
+ FileUtils.mkdir_p(File.join(build_dir, 'subdir', 'nested'))
245
+ File.write(File.join(build_dir, 'subdir', 'file.txt'), 'content')
246
+
247
+ created_paths = []
248
+ allow(Middleman::S3Sync::Resource).to receive(:new) do |resource, remote, path: nil|
249
+ created_paths << path
250
+ mock_resource = double('resource', status: :new, path: path)
251
+ allow(mock_resource).to receive(:tap).and_yield(mock_resource).and_return(mock_resource)
252
+ mock_resource
253
+ end
254
+
255
+ Middleman::S3Sync.send(:discover_orphan_files)
256
+
257
+ expect(created_paths).to include('subdir/file.txt')
258
+ expect(created_paths).not_to include('subdir')
259
+ expect(created_paths).not_to include('subdir/nested')
260
+ end
261
+ end
262
+
263
+ context 'when scan_build_dir is disabled' do
264
+ let(:s3_sync_options) do
265
+ double(
266
+ scan_build_dir: false,
267
+ build_dir: build_dir,
268
+ bucket: 'test-bucket',
269
+ after_s3_sync: nil
270
+ )
271
+ end
272
+
273
+ it 'does not scan for orphan files' do
274
+ File.write(File.join(build_dir, 'orphan.txt'), 'content')
275
+
276
+ expect(Middleman::S3Sync).not_to receive(:discover_orphan_files)
277
+
278
+ # Call work_to_be_done? but mock the heavy parts
279
+ allow(Middleman::S3Sync).to receive(:mm_resources).and_return([])
280
+ allow(Middleman::S3Sync).to receive(:remote_only_paths).and_return([])
281
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
282
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
283
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
284
+
285
+ Middleman::S3Sync.send(:work_to_be_done?)
286
+ end
287
+ end
288
+ end
289
+
134
290
  describe 'batch delete operations' do
135
291
  let(:bucket) { double('bucket') }
136
292
  let(:resource1) { double('resource1', path: 'file1.html', remote_path: 'file1.html') }
@@ -177,7 +333,8 @@ describe 'S3Sync CloudFront Integration' do
177
333
  dry_run_options = double(
178
334
  dry_run: true,
179
335
  delete: true,
180
- bucket: 'test-bucket'
336
+ bucket: 'test-bucket',
337
+ after_s3_sync: nil
181
338
  )
182
339
  allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(dry_run_options)
183
340
  allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([resource1])
@@ -195,4 +352,136 @@ describe 'S3Sync CloudFront Integration' do
195
352
  Middleman::S3Sync.send(:delete_resources)
196
353
  end
197
354
  end
355
+
356
+ describe 'after_s3_sync callback' do
357
+ before do
358
+ Middleman::S3Sync.instance_variable_set(:@app, nil)
359
+
360
+ sitemap = double('sitemap')
361
+ allow(sitemap).to receive(:ensure_resource_list_updated!)
362
+ allow(app).to receive(:respond_to?).with(:sitemap).and_return(true)
363
+ allow(app).to receive(:sitemap).and_return(sitemap)
364
+
365
+ allow(::Middleman::Application).to receive(:new).and_return(app)
366
+ allow(Middleman::S3Sync).to receive(:say_status)
367
+ allow(Middleman::S3Sync).to receive(:work_to_be_done?).and_return(true)
368
+ allow(Middleman::S3Sync).to receive(:update_bucket_versioning)
369
+ allow(Middleman::S3Sync).to receive(:update_bucket_website)
370
+ allow(Middleman::S3Sync).to receive(:ignore_resources)
371
+ allow(Middleman::S3Sync).to receive(:create_resources)
372
+ allow(Middleman::S3Sync).to receive(:update_resources)
373
+ allow(Middleman::S3Sync).to receive(:delete_resources)
374
+ allow(Middleman::S3Sync::CloudFront).to receive(:invalidate)
375
+ end
376
+
377
+ context 'when after_s3_sync callback is provided' do
378
+ it 'executes the callback after sync completes' do
379
+ callback_executed = false
380
+ callback_options = double(
381
+ cloudfront_invalidate: false,
382
+ bucket: 'test-bucket',
383
+ after_s3_sync: -> { callback_executed = true }
384
+ )
385
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
386
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
387
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
388
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
389
+
390
+ Middleman::S3Sync.sync
391
+
392
+ expect(callback_executed).to be true
393
+ end
394
+
395
+ it 'passes sync results to the callback' do
396
+ received_results = nil
397
+ callback_options = double(
398
+ cloudfront_invalidate: false,
399
+ bucket: 'test-bucket',
400
+ after_s3_sync: ->(results) { received_results = results }
401
+ )
402
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
403
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return(['file1.html'])
404
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return(['file2.html', 'file3.html'])
405
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
406
+
407
+ Middleman::S3Sync.sync
408
+
409
+ expect(received_results).to be_a(Hash)
410
+ expect(received_results[:created]).to eq(1)
411
+ expect(received_results[:updated]).to eq(2)
412
+ expect(received_results[:deleted]).to eq(0)
413
+ end
414
+
415
+ it 'logs callback execution status' do
416
+ callback_options = double(
417
+ cloudfront_invalidate: false,
418
+ bucket: 'test-bucket',
419
+ after_s3_sync: -> { 'done' }
420
+ )
421
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(callback_options)
422
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
423
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
424
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
425
+
426
+ expect(Middleman::S3Sync).to receive(:say_status).with('callback', /Running after_s3_sync/)
427
+ expect(Middleman::S3Sync).to receive(:say_status).with('callback', /after_s3_sync completed/)
428
+
429
+ Middleman::S3Sync.sync
430
+ end
431
+
432
+ it 'handles callback errors gracefully' do
433
+ error_callback_options = double(
434
+ cloudfront_invalidate: false,
435
+ bucket: 'test-bucket',
436
+ after_s3_sync: ->(_results) { raise 'Callback error!' }
437
+ )
438
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(error_callback_options)
439
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
440
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
441
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
442
+
443
+ expect(Middleman::S3Sync).to receive(:say_status).with('error', /Callback error!/)
444
+ expect { Middleman::S3Sync.sync }.not_to raise_error
445
+ end
446
+ end
447
+
448
+ context 'when after_s3_sync callback is nil' do
449
+ it 'does not attempt to execute a callback' do
450
+ nil_callback_options = double(
451
+ cloudfront_invalidate: false,
452
+ bucket: 'test-bucket',
453
+ after_s3_sync: nil
454
+ )
455
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(nil_callback_options)
456
+
457
+ # Should not log callback execution
458
+ expect(Middleman::S3Sync).not_to receive(:say_status).with('callback', anything)
459
+
460
+ Middleman::S3Sync.sync
461
+ end
462
+ end
463
+
464
+ context 'when after_s3_sync callback is a method name' do
465
+ it 'executes the method on the app' do
466
+ method_executed = false
467
+ allow(app).to receive(:my_callback) { method_executed = true }
468
+ allow(app).to receive(:respond_to?).with(:my_callback).and_return(true)
469
+ allow(app).to receive(:method).with(:my_callback).and_return(double(arity: 0))
470
+
471
+ method_callback_options = double(
472
+ cloudfront_invalidate: false,
473
+ bucket: 'test-bucket',
474
+ after_s3_sync: :my_callback
475
+ )
476
+ allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(method_callback_options)
477
+ allow(Middleman::S3Sync).to receive(:files_to_create).and_return([])
478
+ allow(Middleman::S3Sync).to receive(:files_to_update).and_return([])
479
+ allow(Middleman::S3Sync).to receive(:files_to_delete).and_return([])
480
+
481
+ Middleman::S3Sync.sync
482
+
483
+ expect(method_executed).to be true
484
+ end
485
+ end
486
+ end
198
487
  end
data/spec/spec_helper.rb CHANGED
@@ -6,7 +6,6 @@
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
7
 
8
8
  require 'middleman-s3_sync'
9
- require 'timerizer'
10
9
  require 'rspec/its'
11
10
  require 'rspec/support'
12
11
  require 'digest/md5'