middleman-s3_sync 4.0.3 → 4.6.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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.s3_sync.sample +20 -0
- data/README.md +216 -40
- data/lib/middleman/redirect.rb +21 -0
- data/lib/middleman/s3_sync/cloudfront.rb +192 -0
- data/lib/middleman/s3_sync/options.rb +6 -0
- data/lib/middleman/s3_sync/resource.rb +129 -24
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +87 -38
- data/lib/middleman-s3_sync/commands.rb +32 -1
- data/lib/middleman-s3_sync/extension.rb +19 -3
- data/lib/middleman-s3_sync.rb +1 -0
- data/middleman-s3_sync.gemspec +9 -5
- data/spec/cloudfront_spec.rb +391 -0
- data/spec/resource_spec.rb +78 -13
- data/spec/s3_sync_integration_spec.rb +132 -0
- data/spec/spec_helper.rb +41 -1
- metadata +77 -19
@@ -0,0 +1,391 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'middleman/s3_sync/cloudfront'
|
3
|
+
|
4
|
+
describe Middleman::S3Sync::CloudFront do
|
5
|
+
let(:options) do
|
6
|
+
double(
|
7
|
+
cloudfront_invalidate: true,
|
8
|
+
cloudfront_distribution_id: 'E1234567890123',
|
9
|
+
cloudfront_invalidate_all: false,
|
10
|
+
cloudfront_invalidation_batch_size: 1000,
|
11
|
+
cloudfront_wait: false,
|
12
|
+
aws_access_key_id: 'test_key',
|
13
|
+
aws_secret_access_key: 'test_secret',
|
14
|
+
aws_session_token: nil,
|
15
|
+
dry_run: false,
|
16
|
+
verbose: false
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:invalidation_response) do
|
21
|
+
double(
|
22
|
+
invalidation: double(id: 'I1234567890123')
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
allow(described_class).to receive(:say_status)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '.invalidate' do
|
31
|
+
context 'when CloudFront invalidation is disabled' do
|
32
|
+
let(:options) do
|
33
|
+
double(cloudfront_invalidate: false)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns early without doing anything' do
|
37
|
+
expect(Aws::CloudFront::Client).not_to receive(:new)
|
38
|
+
result = described_class.invalidate(['/path1', '/path2'], options)
|
39
|
+
expect(result).to be_nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when no distribution ID is provided' do
|
44
|
+
let(:options) do
|
45
|
+
double(
|
46
|
+
cloudfront_invalidate: true,
|
47
|
+
cloudfront_distribution_id: nil
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'skips invalidation and shows warning' do
|
52
|
+
expect(described_class).to receive(:say_status).with(
|
53
|
+
match(/CloudFront invalidation skipped.*no distribution ID/)
|
54
|
+
)
|
55
|
+
expect(Aws::CloudFront::Client).not_to receive(:new)
|
56
|
+
result = described_class.invalidate(['/path1', '/path2'], options)
|
57
|
+
expect(result).to be_nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when dry run is enabled' do
|
62
|
+
let(:options) do
|
63
|
+
double(
|
64
|
+
cloudfront_invalidate: true,
|
65
|
+
cloudfront_distribution_id: 'E1234567890123',
|
66
|
+
cloudfront_invalidate_all: false,
|
67
|
+
dry_run: true,
|
68
|
+
verbose: true
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'shows what would be invalidated without making API calls' do
|
73
|
+
expect(described_class).to receive(:say_status).with(
|
74
|
+
'Invalidating CloudFront distribution E1234567890123'
|
75
|
+
)
|
76
|
+
expect(described_class).to receive(:say_status).with(
|
77
|
+
match(/DRY RUN.*Would invalidate 2 paths/)
|
78
|
+
)
|
79
|
+
expect(described_class).to receive(:say_status).with(' /path1')
|
80
|
+
expect(described_class).to receive(:say_status).with(' /path2')
|
81
|
+
expect(Aws::CloudFront::Client).not_to receive(:new)
|
82
|
+
|
83
|
+
result = described_class.invalidate(['/path1', '/path2'], options)
|
84
|
+
expect(result).to be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when invalidating all paths' do
|
89
|
+
let(:options) do
|
90
|
+
double(
|
91
|
+
cloudfront_invalidate: true,
|
92
|
+
cloudfront_distribution_id: 'E1234567890123',
|
93
|
+
cloudfront_invalidate_all: true,
|
94
|
+
cloudfront_invalidation_batch_size: 1000,
|
95
|
+
cloudfront_wait: false,
|
96
|
+
aws_access_key_id: 'test_key',
|
97
|
+
aws_secret_access_key: 'test_secret',
|
98
|
+
aws_session_token: nil,
|
99
|
+
dry_run: false,
|
100
|
+
verbose: false
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'invalidates all paths with /*' do
|
105
|
+
client = double('cloudfront_client')
|
106
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
107
|
+
expect(client).to receive(:create_invalidation).with({
|
108
|
+
distribution_id: 'E1234567890123',
|
109
|
+
invalidation_batch: {
|
110
|
+
paths: {
|
111
|
+
quantity: 1,
|
112
|
+
items: ['/*']
|
113
|
+
},
|
114
|
+
caller_reference: match(/middleman-s3_sync-\d+-[a-f0-9]{8}/)
|
115
|
+
}
|
116
|
+
}).and_return(invalidation_response)
|
117
|
+
|
118
|
+
result = described_class.invalidate(['/path1', '/path2'], options)
|
119
|
+
expect(result).to eq(['I1234567890123'])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'when invalidating specific paths' do
|
124
|
+
it 'creates invalidation for the provided paths' do
|
125
|
+
client = double('cloudfront_client')
|
126
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
127
|
+
expect(client).to receive(:create_invalidation).with({
|
128
|
+
distribution_id: 'E1234567890123',
|
129
|
+
invalidation_batch: {
|
130
|
+
paths: {
|
131
|
+
quantity: 2,
|
132
|
+
items: ['/path1', '/path2']
|
133
|
+
},
|
134
|
+
caller_reference: match(/middleman-s3_sync-\d+-[a-f0-9]{8}/)
|
135
|
+
}
|
136
|
+
}).and_return(invalidation_response)
|
137
|
+
|
138
|
+
result = described_class.invalidate(['/path1', '/path2'], options)
|
139
|
+
expect(result).to eq(['I1234567890123'])
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'normalizes paths to start with /' do
|
143
|
+
client = double('cloudfront_client')
|
144
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
145
|
+
expect(client).to receive(:create_invalidation).with({
|
146
|
+
distribution_id: 'E1234567890123',
|
147
|
+
invalidation_batch: {
|
148
|
+
paths: {
|
149
|
+
quantity: 2,
|
150
|
+
items: ['/path1', '/path2']
|
151
|
+
},
|
152
|
+
caller_reference: match(/middleman-s3_sync-\d+-[a-f0-9]{8}/)
|
153
|
+
}
|
154
|
+
}).and_return(invalidation_response)
|
155
|
+
|
156
|
+
result = described_class.invalidate(['path1', '/path2'], options)
|
157
|
+
expect(result).to eq(['I1234567890123'])
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'removes duplicate paths' do
|
161
|
+
client = double('cloudfront_client')
|
162
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
163
|
+
expect(client).to receive(:create_invalidation).with({
|
164
|
+
distribution_id: 'E1234567890123',
|
165
|
+
invalidation_batch: {
|
166
|
+
paths: {
|
167
|
+
quantity: 2,
|
168
|
+
items: ['/path1', '/path2']
|
169
|
+
},
|
170
|
+
caller_reference: match(/middleman-s3_sync-\d+-[a-f0-9]{8}/)
|
171
|
+
}
|
172
|
+
}).and_return(invalidation_response)
|
173
|
+
|
174
|
+
result = described_class.invalidate(['/path1', 'path1', '/path2'], options)
|
175
|
+
expect(result).to eq(['I1234567890123'])
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'removes double slashes from paths' do
|
179
|
+
client = double('cloudfront_client')
|
180
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
181
|
+
expect(client).to receive(:create_invalidation).with({
|
182
|
+
distribution_id: 'E1234567890123',
|
183
|
+
invalidation_batch: {
|
184
|
+
paths: {
|
185
|
+
quantity: 1,
|
186
|
+
items: ['/path/to/file.html']
|
187
|
+
},
|
188
|
+
caller_reference: match(/middleman-s3_sync-\d+-[a-f0-9]{8}/)
|
189
|
+
}
|
190
|
+
}).and_return(invalidation_response)
|
191
|
+
|
192
|
+
result = described_class.invalidate(['//path//to//file.html'], options)
|
193
|
+
expect(result).to eq(['I1234567890123'])
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context 'with large batch sizes' do
|
198
|
+
let(:options) do
|
199
|
+
double(
|
200
|
+
cloudfront_invalidate: true,
|
201
|
+
cloudfront_distribution_id: 'E1234567890123',
|
202
|
+
cloudfront_invalidate_all: false,
|
203
|
+
cloudfront_invalidation_batch_size: 2,
|
204
|
+
cloudfront_wait: false,
|
205
|
+
aws_access_key_id: 'test_key',
|
206
|
+
aws_secret_access_key: 'test_secret',
|
207
|
+
aws_session_token: nil,
|
208
|
+
dry_run: false,
|
209
|
+
verbose: false
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'splits paths into multiple batches' do
|
214
|
+
client = double('cloudfront_client')
|
215
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
216
|
+
paths = ['/path1', '/path2', '/path3', '/path4']
|
217
|
+
|
218
|
+
expect(client).to receive(:create_invalidation).twice.and_return(invalidation_response)
|
219
|
+
expect(described_class).to receive(:sleep).with(1)
|
220
|
+
|
221
|
+
result = described_class.invalidate(paths, options)
|
222
|
+
expect(result).to eq(['I1234567890123', 'I1234567890123'])
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context 'when CloudFront API returns an error' do
|
227
|
+
let(:error) { Aws::CloudFront::Errors::ServiceError.new(nil, 'Distribution not found') }
|
228
|
+
|
229
|
+
it 'handles API errors gracefully' do
|
230
|
+
client = double('cloudfront_client')
|
231
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
232
|
+
expect(client).to receive(:create_invalidation).and_raise(error)
|
233
|
+
expect(described_class).to receive(:say_status).with(
|
234
|
+
match(/Failed to create CloudFront invalidation.*Distribution not found/)
|
235
|
+
)
|
236
|
+
|
237
|
+
expect {
|
238
|
+
described_class.invalidate(['/path1'], options)
|
239
|
+
}.to raise_error(Aws::CloudFront::Errors::ServiceError)
|
240
|
+
end
|
241
|
+
|
242
|
+
context 'when verbose mode is enabled' do
|
243
|
+
let(:options) do
|
244
|
+
double(
|
245
|
+
cloudfront_invalidate: true,
|
246
|
+
cloudfront_distribution_id: 'E1234567890123',
|
247
|
+
cloudfront_invalidate_all: false,
|
248
|
+
cloudfront_invalidation_batch_size: 1000,
|
249
|
+
cloudfront_wait: false,
|
250
|
+
aws_access_key_id: 'test_key',
|
251
|
+
aws_secret_access_key: 'test_secret',
|
252
|
+
aws_session_token: nil,
|
253
|
+
dry_run: false,
|
254
|
+
verbose: true
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'does not re-raise errors in verbose mode' do
|
259
|
+
client = double('cloudfront_client')
|
260
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
261
|
+
expect(client).to receive(:create_invalidation).and_raise(error)
|
262
|
+
expect(described_class).to receive(:say_status).with(
|
263
|
+
match(/Failed to create CloudFront invalidation/)
|
264
|
+
)
|
265
|
+
|
266
|
+
expect {
|
267
|
+
described_class.invalidate(['/path1'], options)
|
268
|
+
}.not_to raise_error
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
context 'with empty paths and invalidate_all false' do
|
274
|
+
it 'returns early without making API calls' do
|
275
|
+
expect(Aws::CloudFront::Client).not_to receive(:new)
|
276
|
+
result = described_class.invalidate([], options)
|
277
|
+
expect(result).to be_nil
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
context 'when cloudfront_wait is enabled' do
|
282
|
+
let(:options) do
|
283
|
+
double(
|
284
|
+
cloudfront_invalidate: true,
|
285
|
+
cloudfront_distribution_id: 'E1234567890123',
|
286
|
+
cloudfront_invalidate_all: false,
|
287
|
+
cloudfront_invalidation_batch_size: 1000,
|
288
|
+
cloudfront_wait: true,
|
289
|
+
aws_access_key_id: 'test_key',
|
290
|
+
aws_secret_access_key: 'test_secret',
|
291
|
+
aws_session_token: nil,
|
292
|
+
dry_run: false,
|
293
|
+
verbose: false
|
294
|
+
)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'waits for invalidation to complete' do
|
298
|
+
client = double('cloudfront_client')
|
299
|
+
allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
|
300
|
+
expect(client).to receive(:create_invalidation).and_return(invalidation_response)
|
301
|
+
expect(client).to receive(:wait_until).with(:invalidation_completed,
|
302
|
+
distribution_id: 'E1234567890123',
|
303
|
+
id: 'I1234567890123'
|
304
|
+
)
|
305
|
+
expect(described_class).to receive(:say_status).with(
|
306
|
+
'Waiting for CloudFront invalidation(s) to complete...'
|
307
|
+
)
|
308
|
+
expect(described_class).to receive(:say_status).with(
|
309
|
+
'CloudFront invalidation(s) completed successfully'
|
310
|
+
)
|
311
|
+
|
312
|
+
result = described_class.invalidate(['/path1'], options)
|
313
|
+
expect(result).to eq(['I1234567890123'])
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe 'CloudFront client configuration' do
|
319
|
+
it 'creates client with correct credentials' do
|
320
|
+
client = double('cloudfront_client')
|
321
|
+
expect(Aws::CloudFront::Client).to receive(:new).with({
|
322
|
+
region: 'us-east-1',
|
323
|
+
access_key_id: 'test_key',
|
324
|
+
secret_access_key: 'test_secret'
|
325
|
+
}).and_return(client)
|
326
|
+
|
327
|
+
expect(client).to receive(:create_invalidation).and_return(invalidation_response)
|
328
|
+
|
329
|
+
described_class.invalidate(['/test'], options)
|
330
|
+
end
|
331
|
+
|
332
|
+
context 'with session token' do
|
333
|
+
let(:options) do
|
334
|
+
double(
|
335
|
+
cloudfront_invalidate: true,
|
336
|
+
cloudfront_distribution_id: 'E1234567890123',
|
337
|
+
cloudfront_invalidate_all: false,
|
338
|
+
cloudfront_invalidation_batch_size: 1000,
|
339
|
+
cloudfront_wait: false,
|
340
|
+
aws_access_key_id: 'test_key',
|
341
|
+
aws_secret_access_key: 'test_secret',
|
342
|
+
aws_session_token: 'test_token',
|
343
|
+
dry_run: false,
|
344
|
+
verbose: false
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
348
|
+
it 'includes session token in client configuration' do
|
349
|
+
client = double('cloudfront_client')
|
350
|
+
expect(Aws::CloudFront::Client).to receive(:new).with({
|
351
|
+
region: 'us-east-1',
|
352
|
+
access_key_id: 'test_key',
|
353
|
+
secret_access_key: 'test_secret',
|
354
|
+
session_token: 'test_token'
|
355
|
+
}).and_return(client)
|
356
|
+
|
357
|
+
expect(client).to receive(:create_invalidation).and_return(invalidation_response)
|
358
|
+
|
359
|
+
described_class.invalidate(['/test'], options)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
context 'without explicit credentials' do
|
364
|
+
let(:options) do
|
365
|
+
double(
|
366
|
+
cloudfront_invalidate: true,
|
367
|
+
cloudfront_distribution_id: 'E1234567890123',
|
368
|
+
cloudfront_invalidate_all: false,
|
369
|
+
cloudfront_invalidation_batch_size: 1000,
|
370
|
+
cloudfront_wait: false,
|
371
|
+
aws_access_key_id: nil,
|
372
|
+
aws_secret_access_key: nil,
|
373
|
+
aws_session_token: nil,
|
374
|
+
dry_run: false,
|
375
|
+
verbose: false
|
376
|
+
)
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'creates client without explicit credentials (uses default chain)' do
|
380
|
+
client = double('cloudfront_client')
|
381
|
+
expect(Aws::CloudFront::Client).to receive(:new).with({
|
382
|
+
region: 'us-east-1'
|
383
|
+
}).and_return(client)
|
384
|
+
|
385
|
+
expect(client).to receive(:create_invalidation).and_return(invalidation_response)
|
386
|
+
|
387
|
+
described_class.invalidate(['/test'], options)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
data/spec/resource_spec.rb
CHANGED
@@ -8,13 +8,39 @@ describe Middleman::S3Sync::Resource do
|
|
8
8
|
|
9
9
|
let(:mm_resource) {
|
10
10
|
double(
|
11
|
-
destination_path: 'path/to/resource.html'
|
11
|
+
destination_path: 'path/to/resource.html',
|
12
|
+
content_type: 'text/html'
|
12
13
|
)
|
13
14
|
}
|
15
|
+
|
16
|
+
let(:s3_client) { instance_double(Aws::S3::Client) }
|
17
|
+
let(:s3_resource) { instance_double(Aws::S3::Resource) }
|
18
|
+
let(:bucket) { instance_double(Aws::S3::Bucket) }
|
19
|
+
let(:s3_object) { instance_double(Aws::S3::Object) }
|
20
|
+
|
14
21
|
before do
|
15
22
|
Middleman::S3Sync.s3_sync_options = options
|
23
|
+
|
16
24
|
options.build_dir = "build"
|
17
25
|
options.prefer_gzip = false
|
26
|
+
options.bucket = "test-bucket"
|
27
|
+
options.acl = "public-read"
|
28
|
+
|
29
|
+
allow(Aws::S3::Client).to receive(:new).and_return(s3_client)
|
30
|
+
allow(Aws::S3::Resource).to receive(:new).and_return(s3_resource)
|
31
|
+
allow(s3_resource).to receive(:bucket).and_return(bucket)
|
32
|
+
allow(bucket).to receive(:exists?).and_return(true)
|
33
|
+
allow(bucket).to receive(:object) do |path|
|
34
|
+
# Ensure path has no leading slash
|
35
|
+
path = path.sub(/^\//, '') if path.is_a?(String)
|
36
|
+
s3_object
|
37
|
+
end
|
38
|
+
allow(s3_object).to receive(:head).and_return(nil)
|
39
|
+
allow(s3_object).to receive(:put).and_return(true)
|
40
|
+
allow(s3_object).to receive(:delete).and_return(true)
|
41
|
+
|
42
|
+
# Allow Middleman::S3Sync to use our mocked bucket
|
43
|
+
allow(Middleman::S3Sync).to receive(:bucket).and_return(bucket)
|
18
44
|
end
|
19
45
|
|
20
46
|
context "a new resource" do
|
@@ -23,6 +49,7 @@ describe Middleman::S3Sync::Resource do
|
|
23
49
|
context "without a prefix" do
|
24
50
|
before do
|
25
51
|
allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
|
52
|
+
allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
|
26
53
|
end
|
27
54
|
|
28
55
|
its(:status) { is_expected.to eq :new }
|
@@ -31,11 +58,11 @@ describe Middleman::S3Sync::Resource do
|
|
31
58
|
expect(resource).not_to be_remote
|
32
59
|
end
|
33
60
|
|
34
|
-
it "
|
61
|
+
it "exists locally" do
|
35
62
|
expect(resource).to be_local
|
36
63
|
end
|
37
64
|
|
38
|
-
its(:path) { is_expected.to eq 'path/to/resource.html'}
|
65
|
+
its(:path) { is_expected.to eq 'path/to/resource.html' }
|
39
66
|
its(:local_path) { is_expected.to eq 'build/path/to/resource.html' }
|
40
67
|
its(:remote_path) { is_expected.to eq 'path/to/resource.html' }
|
41
68
|
end
|
@@ -43,6 +70,7 @@ describe Middleman::S3Sync::Resource do
|
|
43
70
|
context "with a prefix set" do
|
44
71
|
before do
|
45
72
|
allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
|
73
|
+
allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
|
46
74
|
options.prefix = "bob"
|
47
75
|
end
|
48
76
|
|
@@ -62,6 +90,9 @@ describe Middleman::S3Sync::Resource do
|
|
62
90
|
context "gzipped" do
|
63
91
|
before do
|
64
92
|
allow(File).to receive(:exist?).with('build/path/to/resource.html.gz').and_return(true)
|
93
|
+
allow(File).to receive(:read).with('build/path/to/resource.html.gz').and_return('gzipped content')
|
94
|
+
allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
|
95
|
+
allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
|
65
96
|
options.prefer_gzip = true
|
66
97
|
end
|
67
98
|
|
@@ -82,12 +113,7 @@ describe Middleman::S3Sync::Resource do
|
|
82
113
|
context "the file does not exist locally" do
|
83
114
|
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
84
115
|
|
85
|
-
let(:remote) {
|
86
|
-
double(
|
87
|
-
key: 'path/to/resource.html',
|
88
|
-
metadata: {}
|
89
|
-
)
|
90
|
-
}
|
116
|
+
let(:remote) { mock_s3_object('path/to/resource.html') }
|
91
117
|
|
92
118
|
before do
|
93
119
|
resource.full_s3_resource = remote
|
@@ -103,7 +129,7 @@ describe Middleman::S3Sync::Resource do
|
|
103
129
|
expect(resource).to be_remote
|
104
130
|
end
|
105
131
|
|
106
|
-
it "
|
132
|
+
it "does not exist locally" do
|
107
133
|
expect(resource).not_to be_local
|
108
134
|
end
|
109
135
|
|
@@ -115,8 +141,9 @@ describe Middleman::S3Sync::Resource do
|
|
115
141
|
context "with a prefix set" do
|
116
142
|
before do
|
117
143
|
allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(false)
|
118
|
-
|
119
|
-
|
144
|
+
remote = mock_s3_object('bob/path/to/resource.html')
|
145
|
+
resource.full_s3_resource = remote
|
146
|
+
options.prefix = "bob/"
|
120
147
|
end
|
121
148
|
|
122
149
|
its(:status) { is_expected.to eq :deleted }
|
@@ -145,7 +172,7 @@ describe Middleman::S3Sync::Resource do
|
|
145
172
|
expect(resource).to be_remote
|
146
173
|
end
|
147
174
|
|
148
|
-
it "
|
175
|
+
it "does not exist locally" do
|
149
176
|
expect(resource).not_to be_local
|
150
177
|
end
|
151
178
|
|
@@ -154,4 +181,42 @@ describe Middleman::S3Sync::Resource do
|
|
154
181
|
its(:remote_path) { is_expected.to eq 'path/to/resource.html' }
|
155
182
|
end
|
156
183
|
end
|
184
|
+
|
185
|
+
context 'An ignored resource' do
|
186
|
+
context "that is local" do
|
187
|
+
|
188
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
189
|
+
|
190
|
+
let(:mm_resource) {
|
191
|
+
double(
|
192
|
+
destination_path: 'ignored/path/to/resource.html',
|
193
|
+
content_type: 'text/html'
|
194
|
+
)
|
195
|
+
}
|
196
|
+
|
197
|
+
before do
|
198
|
+
allow(File).to receive(:exist?).with('build/ignored/path/to/resource.html').and_return(true)
|
199
|
+
allow(File).to receive(:read).with('build/ignored/path/to/resource.html').and_return('test content')
|
200
|
+
options.ignore_paths = [/^ignored/]
|
201
|
+
end
|
202
|
+
|
203
|
+
its(:status) { is_expected.to eq :ignored }
|
204
|
+
end
|
205
|
+
|
206
|
+
context "that is remote" do
|
207
|
+
|
208
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
209
|
+
|
210
|
+
let(:remote) { mock_s3_object('ignored/path/to/resource.html') }
|
211
|
+
|
212
|
+
before do
|
213
|
+
resource.full_s3_resource = remote
|
214
|
+
options.ignore_paths = [/^ignored/]
|
215
|
+
end
|
216
|
+
|
217
|
+
its(:status) { is_expected.to eq :ignored }
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
157
222
|
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'middleman/s3_sync'
|
3
|
+
|
4
|
+
describe 'S3Sync CloudFront Integration' do
|
5
|
+
let(:app) { double('middleman_app') }
|
6
|
+
let(:s3_sync_options) do
|
7
|
+
double(
|
8
|
+
cloudfront_invalidate: true,
|
9
|
+
cloudfront_distribution_id: 'E1234567890123',
|
10
|
+
cloudfront_invalidate_all: false,
|
11
|
+
cloudfront_invalidation_batch_size: 1000,
|
12
|
+
aws_access_key_id: 'test_key',
|
13
|
+
aws_secret_access_key: 'test_secret',
|
14
|
+
aws_session_token: nil,
|
15
|
+
dry_run: false,
|
16
|
+
verbose: false,
|
17
|
+
delete: true,
|
18
|
+
bucket: 'test-bucket'
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
allow(::Middleman::Application).to receive(:new).and_return(app)
|
24
|
+
allow(Middleman::S3Sync).to receive(:s3_sync_options).and_return(s3_sync_options)
|
25
|
+
allow(Middleman::S3Sync).to receive(:say_status)
|
26
|
+
allow(Middleman::S3Sync).to receive(:work_to_be_done?).and_return(true)
|
27
|
+
allow(Middleman::S3Sync).to receive(:update_bucket_versioning)
|
28
|
+
allow(Middleman::S3Sync).to receive(:update_bucket_website)
|
29
|
+
allow(Middleman::S3Sync).to receive(:ignore_resources)
|
30
|
+
allow(Middleman::S3Sync).to receive(:create_resources)
|
31
|
+
allow(Middleman::S3Sync).to receive(:update_resources)
|
32
|
+
allow(Middleman::S3Sync).to receive(:delete_resources)
|
33
|
+
allow(Middleman::S3Sync::CloudFront).to receive(:invalidate)
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'CloudFront invalidation integration' do
|
37
|
+
it 'calls CloudFront invalidation after sync operations' do
|
38
|
+
# Reset invalidation paths for this test
|
39
|
+
Middleman::S3Sync.instance_variable_set(:@invalidation_paths, [])
|
40
|
+
|
41
|
+
expect(Middleman::S3Sync::CloudFront).to receive(:invalidate).with(
|
42
|
+
[], # Initially empty, gets populated during resource operations
|
43
|
+
s3_sync_options
|
44
|
+
)
|
45
|
+
|
46
|
+
Middleman::S3Sync.sync
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'calls CloudFront invalidation with collected paths' do
|
50
|
+
# Mock the methods to inject paths during sync
|
51
|
+
allow(Middleman::S3Sync).to receive(:create_resources) do
|
52
|
+
Middleman::S3Sync.add_invalidation_path('/updated/file.html')
|
53
|
+
end
|
54
|
+
allow(Middleman::S3Sync).to receive(:update_resources) do
|
55
|
+
Middleman::S3Sync.add_invalidation_path('/new/file.css')
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(Middleman::S3Sync::CloudFront).to receive(:invalidate) do |paths, options|
|
59
|
+
expect(paths).to include('/updated/file.html')
|
60
|
+
expect(paths).to include('/new/file.css')
|
61
|
+
expect(options).to eq(s3_sync_options)
|
62
|
+
end
|
63
|
+
|
64
|
+
Middleman::S3Sync.sync
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when cloudfront_invalidate_all is true' do
|
68
|
+
let(:s3_sync_options) do
|
69
|
+
double(
|
70
|
+
cloudfront_invalidate: true,
|
71
|
+
cloudfront_distribution_id: 'E1234567890123',
|
72
|
+
cloudfront_invalidate_all: true,
|
73
|
+
bucket: 'test-bucket'
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'still calls CloudFront invalidation even when no work is needed' do
|
78
|
+
allow(Middleman::S3Sync).to receive(:work_to_be_done?).and_return(false)
|
79
|
+
|
80
|
+
expect(Middleman::S3Sync::CloudFront).to receive(:invalidate).with(
|
81
|
+
[], s3_sync_options
|
82
|
+
)
|
83
|
+
|
84
|
+
Middleman::S3Sync.sync
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when CloudFront invalidation is disabled' do
|
89
|
+
let(:s3_sync_options) do
|
90
|
+
double(
|
91
|
+
cloudfront_invalidate: false,
|
92
|
+
bucket: 'test-bucket'
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'does not call CloudFront invalidation' do
|
97
|
+
expect(Middleman::S3Sync::CloudFront).not_to receive(:invalidate)
|
98
|
+
|
99
|
+
Middleman::S3Sync.sync
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'path tracking during resource operations' do
|
105
|
+
before do
|
106
|
+
# Reset invalidation paths before each path tracking test
|
107
|
+
Middleman::S3Sync.instance_variable_set(:@invalidation_paths, [])
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'adds paths to invalidation list when resources are processed' do
|
111
|
+
# Test the add_invalidation_path method directly
|
112
|
+
Middleman::S3Sync.add_invalidation_path('test/file.html')
|
113
|
+
Middleman::S3Sync.add_invalidation_path('images/photo.jpg')
|
114
|
+
|
115
|
+
expect(Middleman::S3Sync.invalidation_paths).to include('/test/file.html')
|
116
|
+
expect(Middleman::S3Sync.invalidation_paths).to include('/images/photo.jpg')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'normalizes paths when adding to invalidation list' do
|
120
|
+
Middleman::S3Sync.add_invalidation_path('no-leading-slash.html')
|
121
|
+
|
122
|
+
expect(Middleman::S3Sync.invalidation_paths).to include('/no-leading-slash.html')
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'does not add duplicate paths' do
|
126
|
+
Middleman::S3Sync.add_invalidation_path('/same/path.html')
|
127
|
+
Middleman::S3Sync.add_invalidation_path('/same/path.html')
|
128
|
+
|
129
|
+
expect(Middleman::S3Sync.invalidation_paths.count('/same/path.html')).to eq(1)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|