duple 0.0.1

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.
@@ -0,0 +1,3 @@
1
+ module Duple
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::CLI::Config do
4
+ it 'prints the configuration' do
5
+ result = capture_stdout do
6
+ script = Duple::CLI::Config.new
7
+ script.invoke(:all, [], {})
8
+ end
9
+
10
+ result.should =~ /Environments/
11
+ result.should =~ /Groups/
12
+ result.should =~ /Pre-Refresh Tasks/
13
+ result.should =~ /Post-Refresh Tasks/
14
+ result.should =~ /Other Options/
15
+ end
16
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::CLI::Copy do
4
+ include Duple::CLISpecHelpers
5
+
6
+ def invoke_copy(options = nil)
7
+ options ||= {}
8
+ options = { tables: ['categories'] }.merge(options)
9
+ invoke_cli(:copy, options)
10
+ end
11
+
12
+ before {
13
+ stub_fetch_config
14
+ stub_dump_data
15
+ stub_restore_data
16
+ }
17
+
18
+ context 'with neither tables nor group option' do
19
+ it 'fetches the source credentials' do
20
+ expect {
21
+ invoke_copy(tables: nil)
22
+ }.to raise_error(ArgumentError, 'One of --group or --tables options is required.')
23
+ end
24
+ end
25
+
26
+ context 'from heroku to local' do
27
+ let(:source) { 'stage' }
28
+ let(:target) { 'development' }
29
+
30
+ it 'fetches the source credentials' do
31
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
32
+ .and_return(heroku_config_response)
33
+
34
+ invoke_copy
35
+ end
36
+
37
+ it 'dowloads the data from the source' do
38
+ runner.should_receive(:run)
39
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc -a -t categories -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/stage-data.dump})
40
+
41
+ invoke_copy
42
+ end
43
+
44
+ it 'uploads the data to the target' do
45
+ runner.should_receive(:run)
46
+ .with(%{PGPASSWORD="" pg_restore -e -v --no-acl -O -a -h localhost -U postgres -p 5432 -d duple_development < tmp/duple/stage-data.dump})
47
+
48
+ invoke_copy
49
+ end
50
+ end
51
+
52
+ context 'from heroku to heroku' do
53
+ let(:source) { 'production' }
54
+ let(:target) { 'stage' }
55
+
56
+ it 'fetches the source credentials' do
57
+ runner.should_receive(:capture).with("heroku config -a duple-production")
58
+ .and_return(heroku_config_response)
59
+
60
+ invoke_copy
61
+ end
62
+
63
+ it 'fetches the target credentials' do
64
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
65
+ .and_return(heroku_config_response)
66
+
67
+ invoke_copy
68
+ end
69
+
70
+ it 'dowloads the data from the source' do
71
+ runner.should_receive(:run)
72
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc -a -t categories -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/production-data.dump})
73
+
74
+ invoke_copy
75
+ end
76
+
77
+ it 'uploads the data to the target' do
78
+ runner.should_receive(:run)
79
+ .with(%{PGPASSWORD="pg-pass" pg_restore -e -v --no-acl -O -a -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/production-data.dump})
80
+
81
+ invoke_copy
82
+ end
83
+ end
84
+
85
+ context 'from local to heroku' do
86
+ let(:source) { 'development' }
87
+ let(:target) { 'stage' }
88
+
89
+ it 'fetches the target credentials' do
90
+ runner.should_receive(:capture).with("heroku config -a duple-stage")
91
+ .and_return(heroku_config_response)
92
+
93
+ invoke_copy
94
+ end
95
+
96
+ it 'dumps the data from the local db' do
97
+ stub_fetch_config
98
+ runner.should_receive(:run)
99
+ .with(%{PGPASSWORD="" pg_dump -Fc -a -t categories -h localhost -U postgres -p 5432 duple_development > tmp/duple/development-data.dump})
100
+ stub_restore_data
101
+
102
+ invoke_copy
103
+ end
104
+
105
+ it 'uploads the data to the target' do
106
+ runner.should_receive(:run)
107
+ .with(%{PGPASSWORD="pg-pass" pg_restore -e -v --no-acl -O -a -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/development-data.dump})
108
+
109
+ invoke_copy
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::CLI::Init do
4
+ before { suppress_output }
5
+ after { reset_output }
6
+
7
+ before { FileUtils.mkdir_p('tmp/duple/init') }
8
+ after { FileUtils.rm_rf('tmp/duple') }
9
+
10
+ it 'creates a config file in the default location' do
11
+ FileUtils.chdir('tmp/duple/init') do
12
+ # capture_stdout do
13
+ script = Duple::CLI::Root.new
14
+ script.invoke(:init)
15
+ Pathname.new("config/duple.yml").should exist
16
+ # end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,413 @@
1
+ require 'spec_helper'
2
+
3
+ describe Duple::CLI::Refresh do
4
+ include Duple::CLISpecHelpers
5
+
6
+ def invoke_refresh(options = nil)
7
+ invoke_cli(:refresh, options)
8
+ end
9
+
10
+ before { FileUtils.mkdir_p('tmp/duple') }
11
+ after { FileUtils.rm_rf('tmp/duple') }
12
+
13
+ context 'from heroku to heroku' do
14
+ before {
15
+ stub_fetch_url
16
+ stub_reset_heroku
17
+ stub_restore_url
18
+ }
19
+
20
+ let(:source) { 'production' }
21
+ let(:target) { 'stage' }
22
+
23
+ it 'runs commands in the correct order' do
24
+ runner.should_receive(:run).once.ordered.with(/heroku pgbackups:capture/)
25
+ runner.should_receive(:capture).once.ordered.with(/heroku pgbackups:url/).and_return(heroku_pgbackups_url_response)
26
+ runner.should_receive(:run).once.ordered.with(/heroku pg:reset/)
27
+ runner.should_receive(:run).once.ordered.with(/heroku pgbackups:restore/)
28
+
29
+ invoke_refresh(capture: true)
30
+ end
31
+
32
+ it 'fetches the latest snapshot URL for the source' do
33
+ runner.should_receive(:capture).with('heroku pgbackups:url -a duple-production')
34
+ .and_return(heroku_pgbackups_url_response)
35
+
36
+ invoke_refresh
37
+ end
38
+
39
+ it 'resets the target database' do
40
+ runner.should_receive(:run).with(%{heroku pg:reset -a duple-stage})
41
+
42
+ invoke_refresh
43
+ end
44
+
45
+ it 'does not reset the source database' do
46
+ runner.should_not_receive(:run).with(/heroku pg:reset -a phsnap-production/)
47
+
48
+ invoke_refresh
49
+ end
50
+
51
+ it 'restores the target from the snapshot URL' do
52
+ db_url = heroku_pgbackups_url_response.strip
53
+ runner.should_receive(:run).with(%{heroku pgbackups:restore DATABASE #{db_url} -a duple-stage})
54
+
55
+ invoke_refresh
56
+ end
57
+
58
+ it 'does not capture a new snapshot by default' do
59
+ runner.should_not_receive(:run).with(/heroku pgbackups:capture/)
60
+ invoke_refresh
61
+ end
62
+
63
+ context 'with the --capture flag' do
64
+ it 'captures a new snapshot before fetching the URL' do
65
+ runner.should_receive(:run).with('heroku pgbackups:capture -a duple-production')
66
+ invoke_refresh(capture: true)
67
+ end
68
+ end
69
+
70
+ context 'with a table list' do
71
+ before {
72
+ stub_fetch_config
73
+ stub_dump_data
74
+ stub_restore_data
75
+ }
76
+
77
+ it 'runs commands in the correct order' do
78
+ runner.should_not_receive(:capture).with(/heroku pgbackups:url/)
79
+ runner.should_not_receive(:run).with(/heroku pgbackups:restore/)
80
+
81
+ runner.should_receive(:run).once.ordered.with(/heroku pgbackups:capture/)
82
+ runner.should_receive(:capture).once.ordered.with(/heroku config -a duple-production/).and_return(heroku_config_response)
83
+ runner.should_receive(:run).once.ordered.with(/pg_dump/).and_return(heroku_config_response)
84
+ runner.should_receive(:run).once.ordered.with(/heroku pg:reset/)
85
+ runner.should_receive(:capture).once.ordered.with(/heroku config -a duple-stage/).and_return(heroku_config_response)
86
+ runner.should_receive(:run).once.ordered.with(/pg_restore/)
87
+
88
+ invoke_refresh(capture: true, tables: ['categories'])
89
+ end
90
+
91
+ it 'does not download the snapshot' do
92
+ runner.should_not_receive(:capture).with(/pgbackups/)
93
+ runner.should_not_receive(:capture).with(/curl/)
94
+
95
+ invoke_refresh(tables: ['categories'])
96
+ end
97
+
98
+ it 'dumps the tables in the list' do
99
+ runner.should_receive(:run)
100
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc -a -t categories -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/production-data.dump})
101
+
102
+ invoke_refresh(tables: ['categories'])
103
+ end
104
+
105
+ it 'loads the dump file into the target database' do
106
+ runner.should_receive(:run)
107
+ .with(%{PGPASSWORD="pg-pass" pg_restore -e -v --no-acl -O -a -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/production-data.dump})
108
+
109
+ invoke_refresh(tables: ['categories'])
110
+ end
111
+ end
112
+ end
113
+
114
+ context 'from heroku to local' do
115
+ before {
116
+ stub_fetch_url
117
+ stub_fetch_backups
118
+ stub_download_snapshot
119
+ stub_reset_local
120
+ stub_restore_data
121
+ }
122
+
123
+ let(:source) { 'stage' }
124
+ let(:target) { 'development' }
125
+
126
+ it 'runs commands in the correct order' do
127
+ runner.should_receive(:run).once.ordered.with(/heroku pgbackups:capture/)
128
+ runner.should_receive(:capture).once.ordered.with(/heroku pgbackups:url/).and_return(heroku_pgbackups_url_response)
129
+ runner.should_receive(:capture).once.ordered.with(/heroku pgbackups/).and_return(heroku_pgbackups_response)
130
+ runner.should_receive(:run).once.ordered.with(/curl/)
131
+ runner.should_receive(:run).once.ordered.with(/rake db:drop db:create/)
132
+ runner.should_receive(:run).once.ordered.with(/pg_restore/)
133
+
134
+ invoke_refresh(capture: true)
135
+ end
136
+
137
+ it 'fetches the latest snapshot URL for the source' do
138
+ runner.should_receive(:capture).with('heroku pgbackups:url -a duple-stage')
139
+ .and_return(heroku_pgbackups_url_response)
140
+
141
+ invoke_refresh
142
+ end
143
+
144
+ it 'resets the local database' do
145
+ runner.should_receive(:run).with(%{bundle exec rake db:drop db:create})
146
+
147
+ invoke_refresh
148
+ end
149
+
150
+ it 'does not reset the source database' do
151
+ runner.should_not_receive(:run).with(/heroku pg:reset/)
152
+
153
+ invoke_refresh
154
+ end
155
+
156
+ it 'downloads the snapshot from the snapshot URL' do
157
+ runner.should_receive(:run).with(%{curl -o #{snapshot_path} #{heroku_pgbackups_url_response.strip}})
158
+
159
+ invoke_refresh
160
+ end
161
+
162
+ it 'does not re-download the snapshot if the timestamp has not changed' do
163
+ FileUtils.touch(snapshot_path)
164
+
165
+ runner.should_not_receive(:run).with(/curl/)
166
+
167
+ invoke_refresh
168
+ end
169
+
170
+ it 'loads the snapshot file into the local database' do
171
+ runner.should_receive(:run)
172
+ .with(%{PGPASSWORD="" pg_restore -e -v --no-acl -O -a -h localhost -U postgres -p 5432 -d duple_development < #{snapshot_path}})
173
+
174
+ invoke_refresh
175
+ end
176
+
177
+ it 'does not capture a new snapshot by default' do
178
+ runner.should_not_receive(:run).with(/heroku pgbackups:capture/)
179
+ invoke_refresh
180
+ end
181
+
182
+ context 'with the --capture flag' do
183
+ it 'captures a new snapshot before fetching the URL' do
184
+ runner.should_receive(:run).with('heroku pgbackups:capture -a duple-stage')
185
+ invoke_refresh(capture: true)
186
+ end
187
+ end
188
+
189
+ context 'with a table list' do
190
+ let(:table_options) { {tables: ['categories']} }
191
+
192
+ before {
193
+ stub_fetch_config
194
+ stub_dump_data
195
+ stub_restore_data
196
+ }
197
+
198
+ it 'runs commands in the correct order' do
199
+ runner.should_receive(:run).once.ordered.with(/heroku pgbackups:capture/)
200
+ runner.should_receive(:capture).once.ordered.with(/heroku config -a duple-stage/).and_return(heroku_config_response)
201
+ runner.should_receive(:run).once.ordered.with(/pg_dump/)
202
+ runner.should_receive(:run).once.ordered.with(/rake db:drop db:create/)
203
+ runner.should_receive(:run).once.ordered.with(/pg_restore/)
204
+
205
+ invoke_refresh(table_options.merge(capture: true))
206
+ end
207
+
208
+ it 'does not download the snapshot' do
209
+ runner.should_not_receive(:capture).with(/pgbackups/)
210
+ runner.should_not_receive(:capture).with(/curl/)
211
+
212
+ invoke_refresh(table_options)
213
+ end
214
+
215
+ it 'dumps the tables in the list' do
216
+ runner.should_receive(:run)
217
+ .with(%{PGPASSWORD="pg-pass" pg_dump -Fc -a -t categories -h pg-host -U pg-user -p 6022 pg-db > tmp/duple/stage-data.dump})
218
+
219
+ invoke_refresh(table_options)
220
+ end
221
+
222
+ it 'loads the dump file into the target database' do
223
+ runner.should_receive(:run)
224
+ .with(%{PGPASSWORD="" pg_restore -e -v --no-acl -O -a -h localhost -U postgres -p 5432 -d duple_development < tmp/duple/stage-data.dump})
225
+
226
+ invoke_refresh(table_options)
227
+ end
228
+ end
229
+ end
230
+
231
+ context 'from local to heroku' do
232
+ before {
233
+ stub_dump_data
234
+ stub_reset_heroku
235
+ stub_fetch_config
236
+ stub_restore_data
237
+ }
238
+
239
+ let(:source) { 'development' }
240
+ let(:target) { 'stage' }
241
+
242
+ it 'runs commands in the correct order' do
243
+ runner.should_receive(:run).once.ordered.with(/pg_dump/)
244
+ runner.should_receive(:run).once.ordered.with(/heroku pg:reset/)
245
+ runner.should_receive(:capture).once.ordered.with(/heroku config -a duple-stage/).and_return(heroku_config_response)
246
+ runner.should_receive(:run).once.ordered.with(/pg_restore/)
247
+
248
+ invoke_refresh(capture: true)
249
+ end
250
+
251
+ it 'dumps the data from the local database' do
252
+ runner.should_receive(:run)
253
+ .with(%{PGPASSWORD="" pg_dump -Fc -a -h localhost -U postgres -p 5432 duple_development > tmp/duple/development-data.dump})
254
+
255
+ invoke_refresh
256
+ end
257
+
258
+ it 'resets the target database' do
259
+ runner.should_receive(:run).with(%{heroku pg:reset -a duple-stage})
260
+
261
+ invoke_refresh
262
+ end
263
+
264
+ it 'restores the target from the dump' do
265
+ runner.should_receive(:run)
266
+ .with(%{PGPASSWORD="pg-pass" pg_restore -e -v --no-acl -O -a -h pg-host -U pg-user -p 6022 -d pg-db < tmp/duple/development-data.dump})
267
+
268
+ invoke_refresh
269
+ end
270
+
271
+ it 'does not capture a new snapshot' do
272
+ runner.should_not_receive(:run).with(/heroku pgbackups:capture/)
273
+
274
+ invoke_refresh
275
+ end
276
+
277
+ context 'with the --capture flag' do
278
+ it 'does not capture a new snapshot' do
279
+ runner.should_not_receive(:run).with(/heroku pgbackups:capture/)
280
+
281
+ invoke_refresh(capture: true)
282
+ end
283
+ end
284
+
285
+ context 'with a table list' do
286
+ it 'downloads only the tables in the list' do
287
+ runner.should_receive(:run)
288
+ .with(%{PGPASSWORD="" pg_dump -Fc -a -t categories -h localhost -U postgres -p 5432 duple_development > tmp/duple/development-data.dump})
289
+
290
+ invoke_refresh(tables: ['categories'])
291
+ end
292
+ end
293
+ end
294
+
295
+ context 'with pre-refresh tasks' do
296
+ before { pending 'Implement pre-refresh tasks' }
297
+
298
+ before {
299
+ stub_fetch_url
300
+ stub_fetch_config
301
+ stub_fetch_backups
302
+ stub_dump_data
303
+ stub_download_snapshot
304
+ stub_reset_heroku
305
+ stub_reset_local
306
+ stub_restore_url
307
+ stub_restore_data
308
+ }
309
+
310
+ let(:source) { 'production' }
311
+ let(:target) { 'stage' }
312
+ let(:task_options) { { config: 'spec/config/tasks.yml' } }
313
+
314
+ it 'executes the tasks before refreshing' do
315
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
316
+ runner.should_receive(:run).any_number_of_times.ordered.with(/heroku pgbackups/)
317
+
318
+ invoke_refresh(table_options.merge(capture: true))
319
+ end
320
+
321
+ it 'executes the tasks in order' do
322
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:prepare" -a duple-production')
323
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
324
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:prepare" -a duple-stage')
325
+
326
+ invoke_refresh(task_options)
327
+ end
328
+
329
+ context 'with a local target' do
330
+ let(:target) { 'development' }
331
+
332
+ it 'executes the tasks in order' do
333
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:prepare" -a duple-production')
334
+ runner.should_not_receive(:run).with(/heroku maintenance:on/)
335
+ runner.should_receive(:run).once.ordered.with('rake refresh:prepare')
336
+
337
+ invoke_refresh(task_options)
338
+ end
339
+ end
340
+
341
+ context 'with a local source' do
342
+ let(:source) { 'development' }
343
+
344
+ it 'executes the tasks in order' do
345
+ runner.should_receive(:run).once.ordered.with('rake refresh:prepare')
346
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
347
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:prepare" -a duple-stage')
348
+
349
+ invoke_refresh(task_options)
350
+ end
351
+ end
352
+ end
353
+
354
+ context 'with post-refresh tasks' do
355
+ before { pending 'Implement post-refresh tasks' }
356
+ before {
357
+ stub_fetch_url
358
+ stub_fetch_config
359
+ stub_fetch_backups
360
+ stub_dump_data
361
+ stub_download_snapshot
362
+ stub_reset_heroku
363
+ stub_reset_local
364
+ stub_restore_url
365
+ stub_restore_data
366
+ }
367
+
368
+ let(:source) { 'production' }
369
+ let(:target) { 'stage' }
370
+ let(:task_options) { { config: 'spec/config/tasks.yml' } }
371
+
372
+ it 'executes the tasks after refreshing' do
373
+ runner.should_receive(:run).any_number_of_times.ordered.with(/heroku pgbackups/)
374
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
375
+
376
+ invoke_refresh(table_options.merge(capture: true))
377
+ end
378
+
379
+ it 'executes the tasks in order' do
380
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:finish" -a duple-production')
381
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:off -a duple-stage')
382
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:finish" -a duple-stage')
383
+
384
+ invoke_refresh(task_options)
385
+ end
386
+
387
+ context 'with a local target' do
388
+ let(:target) { 'development' }
389
+
390
+ it 'executes the tasks in order' do
391
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:finish" -a duple-production')
392
+ runner.should_not_receive(:run).with(/heroku maintenance:off/)
393
+ runner.should_receive(:run).once.ordered.with('rake refresh:finish')
394
+
395
+ invoke_refresh(task_options)
396
+ end
397
+ end
398
+
399
+ context 'with a local source' do
400
+ let(:source) { 'development' }
401
+
402
+ it 'executes the tasks in order' do
403
+ runner.should_receive(:run).once.ordered.with('rake refresh:finish')
404
+ runner.should_receive(:run).once.ordered.with('heroku maintenance:on -a duple-stage')
405
+ runner.should_receive(:run).once.ordered.with('heroku run "rake refresh:finish" -a duple-stage')
406
+
407
+ invoke_refresh(task_options)
408
+ end
409
+ end
410
+ end
411
+
412
+
413
+ end