middleman-s3_sync 4.6.4 → 4.7.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 +4 -4
- data/.github/workflows/ci.yml +29 -0
- data/.github/workflows/release.yml +53 -0
- data/Changelog.md +12 -0
- data/README.md +88 -0
- data/WARP.md +5 -1
- data/lib/middleman/s3_sync/cloudfront.rb +50 -26
- data/lib/middleman/s3_sync/options.rb +12 -1
- data/lib/middleman/s3_sync/resource.rb +72 -25
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +163 -13
- data/lib/middleman-s3_sync/commands.rb +3 -1
- data/lib/middleman-s3_sync/extension.rb +22 -5
- data/middleman-s3_sync.gemspec +22 -18
- data/spec/aws_sdk_parameters_spec.rb +70 -6
- data/spec/cloudfront_spec.rb +2 -0
- data/spec/indifferent_hash_spec.rb +278 -0
- data/spec/resource_spec.rb +206 -0
- data/spec/s3_sync_integration_spec.rb +362 -7
- metadata +80 -64
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Middleman::S3Sync::IndifferentHash do
|
|
4
|
+
let(:hash) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe 'string-indifferent key access' do
|
|
7
|
+
it 'allows setting and retrieving values using string keys' do
|
|
8
|
+
hash['foo'] = 'bar'
|
|
9
|
+
expect(hash['foo']).to eq('bar')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'retrieves string key values using symbol keys' do
|
|
13
|
+
hash['foo'] = 'bar'
|
|
14
|
+
expect(hash[:foo]).to eq('bar')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'normalizes string keys on retrieval' do
|
|
18
|
+
hash['foo'] = 'value1'
|
|
19
|
+
hash['foo'] = 'value2'
|
|
20
|
+
expect(hash['foo']).to eq('value2')
|
|
21
|
+
expect(hash.keys.count).to eq(1)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'symbol-indifferent key access' do
|
|
26
|
+
it 'allows setting and retrieving values using symbol keys' do
|
|
27
|
+
hash[:foo] = 'bar'
|
|
28
|
+
expect(hash[:foo]).to eq('bar')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'retrieves symbol key values using string keys' do
|
|
32
|
+
hash[:foo] = 'bar'
|
|
33
|
+
expect(hash['foo']).to eq('bar')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'normalizes symbol keys to strings' do
|
|
37
|
+
hash[:foo] = 'value1'
|
|
38
|
+
hash['foo'] = 'value2'
|
|
39
|
+
expect(hash[:foo]).to eq('value2')
|
|
40
|
+
expect(hash.keys.count).to eq(1)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'stores keys as strings internally' do
|
|
44
|
+
hash[:foo] = 'bar'
|
|
45
|
+
expect(hash.keys).to eq(['foo'])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe 'dot notation access' do
|
|
50
|
+
it 'allows accessing values using dot notation' do
|
|
51
|
+
hash[:foo] = 'bar'
|
|
52
|
+
expect(hash.foo).to eq('bar')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'allows setting values using dot notation' do
|
|
56
|
+
hash.foo = 'baz'
|
|
57
|
+
expect(hash[:foo]).to eq('baz')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'works with string keys' do
|
|
61
|
+
hash['foo'] = 'bar'
|
|
62
|
+
expect(hash.foo).to eq('bar')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'raises NoMethodError for non-existent keys' do
|
|
66
|
+
expect { hash.nonexistent_key }.to raise_error(NoMethodError)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'supports respond_to? for existing keys' do
|
|
70
|
+
hash[:foo] = 'bar'
|
|
71
|
+
expect(hash).to respond_to(:foo)
|
|
72
|
+
expect(hash).to respond_to(:foo=)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns false for respond_to? on non-existent keys' do
|
|
76
|
+
expect(hash).not_to respond_to(:nonexistent)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe 'nested hashes with indifferent access' do
|
|
81
|
+
it 'handles nested hash values' do
|
|
82
|
+
hash[:outer] = { inner: 'value' }
|
|
83
|
+
expect(hash[:outer]).to eq({ inner: 'value' })
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'allows nested IndifferentHash instances' do
|
|
87
|
+
inner = described_class.new
|
|
88
|
+
inner[:foo] = 'bar'
|
|
89
|
+
hash[:outer] = inner
|
|
90
|
+
|
|
91
|
+
expect(hash[:outer][:foo]).to eq('bar')
|
|
92
|
+
expect(hash[:outer].foo).to eq('bar')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'supports multiple levels of nesting' do
|
|
96
|
+
inner = described_class.new
|
|
97
|
+
inner[:level2] = 'deep'
|
|
98
|
+
hash[:level1] = inner
|
|
99
|
+
|
|
100
|
+
expect(hash['level1']['level2']).to eq('deep')
|
|
101
|
+
expect(hash[:level1][:level2]).to eq('deep')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe 'Hash method compatibility' do
|
|
106
|
+
describe '#has_key?' do
|
|
107
|
+
it 'works with string keys' do
|
|
108
|
+
hash['foo'] = 'bar'
|
|
109
|
+
expect(hash.has_key?('foo')).to be true
|
|
110
|
+
expect(hash.has_key?(:foo)).to be true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'works with symbol keys' do
|
|
114
|
+
hash[:foo] = 'bar'
|
|
115
|
+
expect(hash.has_key?('foo')).to be true
|
|
116
|
+
expect(hash.has_key?(:foo)).to be true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'returns false for non-existent keys' do
|
|
120
|
+
expect(hash.has_key?('nonexistent')).to be false
|
|
121
|
+
expect(hash.has_key?(:nonexistent)).to be false
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#key?' do
|
|
126
|
+
it 'is aliased to has_key?' do
|
|
127
|
+
hash[:foo] = 'bar'
|
|
128
|
+
expect(hash.key?('foo')).to be true
|
|
129
|
+
expect(hash.key?(:foo)).to be true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
describe '#include?' do
|
|
134
|
+
it 'is aliased to has_key?' do
|
|
135
|
+
hash[:foo] = 'bar'
|
|
136
|
+
expect(hash.include?('foo')).to be true
|
|
137
|
+
expect(hash.include?(:foo)).to be true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#fetch' do
|
|
142
|
+
it 'fetches values with string keys' do
|
|
143
|
+
hash['foo'] = 'bar'
|
|
144
|
+
expect(hash.fetch('foo')).to eq('bar')
|
|
145
|
+
expect(hash.fetch(:foo)).to eq('bar')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'fetches values with symbol keys' do
|
|
149
|
+
hash[:foo] = 'bar'
|
|
150
|
+
expect(hash.fetch('foo')).to eq('bar')
|
|
151
|
+
expect(hash.fetch(:foo)).to eq('bar')
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'returns default value for missing keys' do
|
|
155
|
+
expect(hash.fetch('missing', 'default')).to eq('default')
|
|
156
|
+
expect(hash.fetch(:missing, 'default')).to eq('default')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'calls block for missing keys' do
|
|
160
|
+
result = hash.fetch('missing') { |key| "Key #{key} not found" }
|
|
161
|
+
expect(result).to eq('Key missing not found')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'raises KeyError when key is missing without default' do
|
|
165
|
+
expect { hash.fetch('missing') }.to raise_error(KeyError)
|
|
166
|
+
expect { hash.fetch(:missing) }.to raise_error(KeyError)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe '.from_hash' do
|
|
172
|
+
it 'creates an IndifferentHash from a regular hash' do
|
|
173
|
+
regular_hash = { 'foo' => 'bar', 'baz' => 'qux' }
|
|
174
|
+
result = described_class.from_hash(regular_hash)
|
|
175
|
+
|
|
176
|
+
expect(result).to be_a(described_class)
|
|
177
|
+
expect(result['foo']).to eq('bar')
|
|
178
|
+
expect(result[:foo]).to eq('bar')
|
|
179
|
+
expect(result['baz']).to eq('qux')
|
|
180
|
+
expect(result[:baz]).to eq('qux')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'handles hashes with symbol keys' do
|
|
184
|
+
regular_hash = { foo: 'bar', baz: 'qux' }
|
|
185
|
+
result = described_class.from_hash(regular_hash)
|
|
186
|
+
|
|
187
|
+
expect(result[:foo]).to eq('bar')
|
|
188
|
+
expect(result['foo']).to eq('bar')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'handles empty hashes' do
|
|
192
|
+
result = described_class.from_hash({})
|
|
193
|
+
expect(result).to be_a(described_class)
|
|
194
|
+
expect(result).to be_empty
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'handles mixed string and symbol keys' do
|
|
198
|
+
regular_hash = { 'string_key' => 'value1', :symbol_key => 'value2' }
|
|
199
|
+
result = described_class.from_hash(regular_hash)
|
|
200
|
+
|
|
201
|
+
expect(result['string_key']).to eq('value1')
|
|
202
|
+
expect(result[:string_key]).to eq('value1')
|
|
203
|
+
expect(result['symbol_key']).to eq('value2')
|
|
204
|
+
expect(result[:symbol_key]).to eq('value2')
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe 'map gem API compatibility' do
|
|
209
|
+
# These tests ensure no breaking changes compared to the map gem
|
|
210
|
+
|
|
211
|
+
it 'supports basic key-value storage' do
|
|
212
|
+
hash[:key] = 'value'
|
|
213
|
+
expect(hash[:key]).to eq('value')
|
|
214
|
+
expect(hash['key']).to eq('value')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'supports dot notation like map gem' do
|
|
218
|
+
hash.max_age = 3600
|
|
219
|
+
expect(hash.max_age).to eq(3600)
|
|
220
|
+
expect(hash[:max_age]).to eq(3600)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'supports fetch with default values' do
|
|
224
|
+
expect(hash.fetch(:missing, 'default')).to eq('default')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'supports has_key? checks' do
|
|
228
|
+
hash[:present] = 'value'
|
|
229
|
+
expect(hash.has_key?(:present)).to be true
|
|
230
|
+
expect(hash.has_key?('present')).to be true
|
|
231
|
+
expect(hash.has_key?(:absent)).to be false
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'maintains Hash inheritance' do
|
|
235
|
+
expect(hash).to be_a(Hash)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'supports standard Hash operations' do
|
|
239
|
+
hash[:a] = 1
|
|
240
|
+
hash[:b] = 2
|
|
241
|
+
|
|
242
|
+
expect(hash.keys.sort).to eq(['a', 'b'])
|
|
243
|
+
expect(hash.values.sort).to eq([1, 2])
|
|
244
|
+
expect(hash.size).to eq(2)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'supports iteration' do
|
|
248
|
+
hash[:a] = 1
|
|
249
|
+
hash[:b] = 2
|
|
250
|
+
|
|
251
|
+
result = []
|
|
252
|
+
hash.each { |k, v| result << [k, v] }
|
|
253
|
+
expect(result).to contain_exactly(['a', 1], ['b', 2])
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
context 'usage in BrowserCachePolicy' do
|
|
257
|
+
it 'supports caching policy access patterns' do
|
|
258
|
+
# Mimics how BrowserCachePolicy uses the hash
|
|
259
|
+
hash[:max_age] = 3600
|
|
260
|
+
hash[:s_maxage] = 7200
|
|
261
|
+
hash[:public] = true
|
|
262
|
+
hash[:no_cache] = false
|
|
263
|
+
|
|
264
|
+
expect(hash.has_key?(:max_age)).to be true
|
|
265
|
+
expect(hash.has_key?('s_maxage')).to be true
|
|
266
|
+
expect(hash.fetch(:public, false)).to be true
|
|
267
|
+
expect(hash.fetch(:private, false)).to be false
|
|
268
|
+
expect(hash.fetch('must_revalidate', false)).to be false
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'supports mixed string and symbol key access like in caching_policy.rb' do
|
|
272
|
+
hash[:max_age] = 3600
|
|
273
|
+
expect(hash['max_age']).to eq(3600)
|
|
274
|
+
expect(hash.max_age).to eq(3600)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
data/spec/resource_spec.rb
CHANGED
|
@@ -182,6 +182,212 @@ describe Middleman::S3Sync::Resource do
|
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
+
context 'content type detection' do
|
|
186
|
+
context 'when mm_resource provides content_type' do
|
|
187
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
188
|
+
|
|
189
|
+
let(:mm_resource) {
|
|
190
|
+
double(
|
|
191
|
+
destination_path: 'path/to/file.html',
|
|
192
|
+
content_type: 'text/html'
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
before do
|
|
197
|
+
allow(File).to receive(:exist?).with('build/path/to/file.html').and_return(true)
|
|
198
|
+
allow(File).to receive(:read).with('build/path/to/file.html').and_return('content')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'uses the content_type from mm_resource' do
|
|
202
|
+
expect(resource.content_type).to eq 'text/html'
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
context 'when mm_resource is nil (orphan file)' do
|
|
207
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
|
208
|
+
|
|
209
|
+
let(:remote) { mock_s3_object('path/to/file.webp') }
|
|
210
|
+
|
|
211
|
+
before do
|
|
212
|
+
resource.full_s3_resource = remote
|
|
213
|
+
allow(File).to receive(:exist?).with('build/path/to/file.webp').and_return(true)
|
|
214
|
+
allow(File).to receive(:read).with('build/path/to/file.webp').and_return('content')
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'falls back to mime-types for known extensions' do
|
|
218
|
+
expect(resource.content_type).to eq 'image/webp'
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
context 'when mm_resource has nil content_type' do
|
|
223
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
224
|
+
|
|
225
|
+
let(:mm_resource) {
|
|
226
|
+
double(
|
|
227
|
+
destination_path: 'path/to/image.svg',
|
|
228
|
+
content_type: nil
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
before do
|
|
233
|
+
allow(File).to receive(:exist?).with('build/path/to/image.svg').and_return(true)
|
|
234
|
+
allow(File).to receive(:read).with('build/path/to/image.svg').and_return('content')
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'falls back to mime-types' do
|
|
238
|
+
expect(resource.content_type).to eq 'image/svg+xml'
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
context 'when content_types option is set' do
|
|
243
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
244
|
+
|
|
245
|
+
let(:mm_resource) {
|
|
246
|
+
double(
|
|
247
|
+
destination_path: 'path/to/data.custom',
|
|
248
|
+
content_type: nil
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
before do
|
|
253
|
+
allow(File).to receive(:exist?).with('build/path/to/data.custom').and_return(true)
|
|
254
|
+
allow(File).to receive(:read).with('build/path/to/data.custom').and_return('content')
|
|
255
|
+
options.content_types = { 'path/to/data.custom' => 'application/x-custom' }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
it 'uses content_types option by path' do
|
|
259
|
+
expect(resource.content_type).to eq 'application/x-custom'
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context 'when content_types option uses local_path key' do
|
|
264
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
265
|
+
|
|
266
|
+
let(:mm_resource) {
|
|
267
|
+
double(
|
|
268
|
+
destination_path: 'path/to/data.custom',
|
|
269
|
+
content_type: nil
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
before do
|
|
274
|
+
allow(File).to receive(:exist?).with('build/path/to/data.custom').and_return(true)
|
|
275
|
+
allow(File).to receive(:read).with('build/path/to/data.custom').and_return('content')
|
|
276
|
+
options.content_types = { 'build/path/to/data.custom' => 'application/x-custom-local' }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'uses content_types option by local_path' do
|
|
280
|
+
expect(resource.content_type).to eq 'application/x-custom-local'
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
context 'when extension is unknown' do
|
|
285
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
|
|
286
|
+
|
|
287
|
+
let(:mm_resource) {
|
|
288
|
+
double(
|
|
289
|
+
destination_path: 'path/to/file.unknownext123',
|
|
290
|
+
content_type: nil
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
before do
|
|
295
|
+
allow(File).to receive(:exist?).with('build/path/to/file.unknownext123').and_return(true)
|
|
296
|
+
allow(File).to receive(:read).with('build/path/to/file.unknownext123').and_return('content')
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'defaults to application/octet-stream' do
|
|
300
|
+
expect(resource.content_type).to eq 'application/octet-stream'
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
context 'a resource with explicit path (orphan file)' do
|
|
306
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, nil, path: 'orphan/file.webp') }
|
|
307
|
+
|
|
308
|
+
before do
|
|
309
|
+
allow(File).to receive(:exist?).with('build/orphan/file.webp').and_return(true)
|
|
310
|
+
allow(File).to receive(:exist?).with('build/orphan/file.webp.gz').and_return(false)
|
|
311
|
+
allow(File).to receive(:read).with('build/orphan/file.webp').and_return('content')
|
|
312
|
+
allow(File).to receive(:directory?).with('build/orphan/file.webp').and_return(false)
|
|
313
|
+
|
|
314
|
+
# Mock the bucket to return NotFound for head request (file doesn't exist on S3)
|
|
315
|
+
mock_object = instance_double(Aws::S3::Object)
|
|
316
|
+
allow(mock_object).to receive(:head).and_raise(Aws::S3::Errors::NotFound.new(nil, 'Not Found'))
|
|
317
|
+
allow(bucket).to receive(:object).with('orphan/file.webp').and_return(mock_object)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
its(:path) { is_expected.to eq 'orphan/file.webp' }
|
|
321
|
+
its(:local_path) { is_expected.to eq 'build/orphan/file.webp' }
|
|
322
|
+
its(:status) { is_expected.to eq :new }
|
|
323
|
+
|
|
324
|
+
it 'detects content type via mime-types' do
|
|
325
|
+
expect(resource.content_type).to eq 'image/webp'
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
context 'redirect preservation' do
|
|
330
|
+
context 'remote-only redirect (no local file)' do
|
|
331
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
|
332
|
+
|
|
333
|
+
let(:remote) do
|
|
334
|
+
double(
|
|
335
|
+
key: 'old-page.html',
|
|
336
|
+
metadata: {},
|
|
337
|
+
etag: '"abc123"',
|
|
338
|
+
content_encoding: nil,
|
|
339
|
+
cache_control: nil,
|
|
340
|
+
website_redirect_location: '/new-page.html'
|
|
341
|
+
)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
before do
|
|
345
|
+
allow(File).to receive(:exist?).with('build/old-page.html').and_return(false)
|
|
346
|
+
resource.full_s3_resource = remote
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'detects as a redirect' do
|
|
350
|
+
expect(resource.redirect?).to be true
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
its(:status) { is_expected.to eq :ignored }
|
|
354
|
+
|
|
355
|
+
it 'is not marked for deletion' do
|
|
356
|
+
expect(resource.to_delete?).to be false
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
context 'remote file without redirect (no local file)' do
|
|
361
|
+
subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
|
|
362
|
+
|
|
363
|
+
let(:remote) do
|
|
364
|
+
double(
|
|
365
|
+
key: 'deleted-page.html',
|
|
366
|
+
metadata: {},
|
|
367
|
+
etag: '"abc123"',
|
|
368
|
+
content_encoding: nil,
|
|
369
|
+
cache_control: nil,
|
|
370
|
+
website_redirect_location: nil
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
before do
|
|
375
|
+
allow(File).to receive(:exist?).with('build/deleted-page.html').and_return(false)
|
|
376
|
+
resource.full_s3_resource = remote
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'is not a redirect' do
|
|
380
|
+
expect(resource.redirect?).to be_falsey
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
its(:status) { is_expected.to eq :deleted }
|
|
384
|
+
|
|
385
|
+
it 'is marked for deletion' do
|
|
386
|
+
expect(resource.to_delete?).to be true
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
185
391
|
context 'An ignored resource' do
|
|
186
392
|
context "that is local" do
|
|
187
393
|
|