datadog_backup 3.0.0.alpha.2 → 3.1.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,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'diffy'
4
+ require 'deepsort'
5
+ require 'faraday'
6
+ require 'faraday/retry'
7
+
8
+ module DatadogBackup
9
+ # The default options for backing up and restores.
10
+ # This base class is meant to be extended by specific resources, such as Dashboards, Monitors, and so on.
11
+ class Resources
12
+ include ::DatadogBackup::LocalFilesystem
13
+ include ::DatadogBackup::Options
14
+
15
+ RETRY_OPTIONS = {
16
+ max: 5,
17
+ interval: 0.05,
18
+ interval_randomness: 0.5,
19
+ backoff_factor: 2
20
+ }.freeze
21
+
22
+ def backup
23
+ raise 'subclass is expected to implement #backup'
24
+ end
25
+
26
+ # Returns the diffy diff.
27
+ # Optionally, supply an array of keys to remove from comparison
28
+ def diff(id)
29
+ current = except(get_by_id(id)).deep_sort.to_yaml
30
+ filesystem = except(load_from_file_by_id(id)).deep_sort.to_yaml
31
+ result = ::Diffy::Diff.new(current, filesystem, include_plus_and_minus_in_html: true).to_s(diff_format)
32
+ LOGGER.debug("Compared ID #{id} and found filesystem: #{filesystem} <=> current: #{current} == result: #{result}")
33
+ result.chomp
34
+ end
35
+
36
+ # Returns a hash with banlist elements removed
37
+ def except(hash)
38
+ hash.tap do # tap returns self
39
+ @banlist.each do |key|
40
+ hash.delete(key) # delete returns the value at the deleted key, hence the tap wrapper
41
+ end
42
+ end
43
+ end
44
+
45
+ # Fetch the specified resource from Datadog
46
+ def get(id)
47
+ params = {}
48
+ headers = {}
49
+ response = api_service.get("/api/#{api_version}/#{api_resource_name}/#{id}", params, headers)
50
+ body_with_2xx(response)
51
+ end
52
+
53
+ # Returns a list of all resources in Datadog
54
+ # Do not use directly, but use the child classes' #all method instead
55
+ def get_all
56
+ return @get_all if @get_all
57
+
58
+ params = {}
59
+ headers = {}
60
+ response = api_service.get("/api/#{api_version}/#{api_resource_name}", params, headers)
61
+ @get_all = body_with_2xx(response)
62
+ end
63
+
64
+ # Download the resource from Datadog and write it to a file
65
+ def get_and_write_file(id)
66
+ body = get_by_id(id)
67
+ write_file(dump(body), filename(id))
68
+ body
69
+ end
70
+
71
+ # Fetch the specified resource from Datadog and remove the banlist elements
72
+ def get_by_id(id)
73
+ except(get(id))
74
+ end
75
+
76
+ def initialize(options)
77
+ @options = options
78
+ @banlist = []
79
+ ::FileUtils.mkdir_p(mydir)
80
+ end
81
+
82
+ def myclass
83
+ self.class.to_s.split(':').last.downcase
84
+ end
85
+
86
+ # Create a new resource in Datadog
87
+ def create(body)
88
+ headers = {}
89
+ response = api_service.post("/api/#{api_version}/#{api_resource_name}", body, headers)
90
+ body = body_with_2xx(response)
91
+ LOGGER.warn "Successfully created #{body.fetch(id_keyname)} in datadog."
92
+ LOGGER.info 'Invalidating cache'
93
+ @get_all = nil
94
+ body
95
+ end
96
+
97
+ # Update an existing resource in Datadog
98
+ def update(id, body)
99
+ headers = {}
100
+ response = api_service.put("/api/#{api_version}/#{api_resource_name}/#{id}", body, headers)
101
+ body = body_with_2xx(response)
102
+ LOGGER.warn "Successfully restored #{id} to datadog."
103
+ LOGGER.info 'Invalidating cache'
104
+ @get_all = nil
105
+ body
106
+ end
107
+
108
+ # If the resource exists in Datadog, update it. Otherwise, create it.
109
+ def restore(id)
110
+ body = load_from_file_by_id(id)
111
+ begin
112
+ update(id, body)
113
+ rescue RuntimeError => e
114
+ raise e.message unless e.message.include?('update failed with error 404')
115
+
116
+ create_newly(id, body)
117
+ end
118
+ end
119
+
120
+ # Return the Faraday body from a response with a 2xx status code, otherwise raise an error
121
+ def body_with_2xx(response)
122
+ unless response.status.to_s =~ /^2/
123
+ raise "#{caller_locations(1,
124
+ 1)[0].label} failed with error #{response.status}"
125
+ end
126
+
127
+ response.body
128
+ end
129
+
130
+ private
131
+
132
+ def api_url
133
+ ENV.fetch('DD_SITE_URL', 'https://api.datadoghq.com/')
134
+ end
135
+
136
+ def api_version
137
+ raise 'subclass is expected to implement #api_version'
138
+ end
139
+
140
+ def api_resource_name
141
+ raise 'subclass is expected to implement #api_resource_name'
142
+ end
143
+
144
+ # Some resources have a different key for the id.
145
+ def id_keyname
146
+ 'id'
147
+ end
148
+
149
+ def api_service
150
+ @api_service ||= Faraday.new(
151
+ url: api_url,
152
+ headers: {
153
+ 'DD-API-KEY' => ENV.fetch('DD_API_KEY'),
154
+ 'DD-APPLICATION-KEY' => ENV.fetch('DD_APP_KEY')
155
+ }
156
+ ) do |faraday|
157
+ faraday.request :json
158
+ faraday.request :retry, RETRY_OPTIONS
159
+ faraday.response(:logger, LOGGER, { headers: true, bodies: LOGGER.debug?, log_level: :debug }) do |logger|
160
+ logger.filter(/(DD-API-KEY:)([^&]+)/, '\1[REDACTED]')
161
+ logger.filter(/(DD-APPLICATION-KEY:)([^&]+)/, '\1[REDACTED]')
162
+ end
163
+ faraday.response :raise_error
164
+ faraday.response :json
165
+ faraday.adapter Faraday.default_adapter
166
+ end
167
+ end
168
+
169
+ # Create a new resource in Datadog, then move the old file to the new resource's ID
170
+ def create_newly(file_id, body)
171
+ new_id = create(body).fetch(id_keyname)
172
+ FileUtils.rm(find_file_by_id(file_id))
173
+ get_and_write_file(new_id)
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatadogBackup
4
+ # Synthetic specific overrides for backup and restore.
5
+ class Synthetics < Resources
6
+ def all
7
+ get_all.fetch('tests')
8
+ end
9
+
10
+ def backup
11
+ all.map do |synthetic|
12
+ id = synthetic[id_keyname]
13
+ get_and_write_file(id)
14
+ end
15
+ end
16
+
17
+ def get_by_id(id)
18
+ synthetic = all.select { |s| s[id_keyname].to_s == id.to_s }.first
19
+ synthetic.nil? ? {} : except(synthetic)
20
+ end
21
+
22
+ def initialize(options)
23
+ super(options)
24
+ @banlist = %w[creator created_at modified_at monitor_id public_id].freeze
25
+ end
26
+
27
+ def create(body)
28
+ create_api_resource_name = api_resource_name(body)
29
+ headers = {}
30
+ response = api_service.post("/api/#{api_version}/#{create_api_resource_name}", body, headers)
31
+ resbody = body_with_2xx(response)
32
+ LOGGER.warn "Successfully created #{resbody.fetch(id_keyname)} in datadog."
33
+ LOGGER.info 'Invalidating cache'
34
+ @get_all = nil
35
+ resbody
36
+ end
37
+
38
+ def update(id, body)
39
+ update_api_resource_name = api_resource_name(body)
40
+ headers = {}
41
+ response = api_service.put("/api/#{api_version}/#{update_api_resource_name}/#{id}", body, headers)
42
+ resbody = body_with_2xx(response)
43
+ LOGGER.warn "Successfully restored #{id} to datadog."
44
+ LOGGER.info 'Invalidating cache'
45
+ @get_all = nil
46
+ resbody
47
+ end
48
+
49
+ private
50
+
51
+ def api_version
52
+ 'v1'
53
+ end
54
+
55
+ def api_resource_name(body = nil)
56
+ return 'synthetics/tests' if body.nil?
57
+ return 'synthetics/tests' if body['type'].nil?
58
+ return 'synthetics/tests/browser' if body['type'].to_s == 'browser'
59
+ return 'synthetics/tests/api' if body['type'].to_s == 'api'
60
+
61
+ raise "Unknown type #{body['type']}"
62
+ end
63
+
64
+ def id_keyname
65
+ 'public_id'
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatadogBackup
4
+ # Used by CLI and Dashboards to size thread pool according to available CPU resourcess.
4
5
  module ThreadPool
5
6
  TPOOL = ::Concurrent::ThreadPoolExecutor.new(
6
7
  min_threads: [2, Concurrent.processor_count].max,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatadogBackup
4
- VERSION = '3.0.0.alpha.2'
4
+ VERSION = '3.1.1'
5
5
  end
@@ -5,15 +5,15 @@ require 'concurrent'
5
5
  require_relative 'datadog_backup/local_filesystem'
6
6
  require_relative 'datadog_backup/options'
7
7
  require_relative 'datadog_backup/cli'
8
- require_relative 'datadog_backup/core'
8
+ require_relative 'datadog_backup/resources'
9
9
  require_relative 'datadog_backup/dashboards'
10
10
  require_relative 'datadog_backup/monitors'
11
+ require_relative 'datadog_backup/synthetics'
11
12
  require_relative 'datadog_backup/thread_pool'
12
13
  require_relative 'datadog_backup/version'
13
14
  require_relative 'datadog_backup/deprecations'
14
15
  DatadogBackup::Deprecations.check
15
16
 
16
-
17
+ # DatadogBackup is a gem for backing up and restoring Datadog monitors and dashboards.
17
18
  module DatadogBackup
18
19
  end
19
-
data/release.config.js CHANGED
@@ -1,4 +1,8 @@
1
1
  module.exports = {
2
+ "branches": [
3
+ '+([0-9])?(.{+([0-9]),x}).x',
4
+ 'main'
5
+ ],
2
6
  "plugins": [
3
7
  "@semantic-release/commit-analyzer",
4
8
  "@semantic-release/release-notes-generator",
@@ -29,16 +29,14 @@ describe DatadogBackup::Cli do
29
29
  describe '#backup' do
30
30
  context 'when dashboards are deleted in datadog' do
31
31
  let(:all_dashboards) do
32
- [
33
- 200,
34
- {},
32
+ respond_with200(
35
33
  {
36
34
  'dashboards' => [
37
35
  { 'id' => 'stillthere' },
38
36
  { 'id' => 'alsostillthere' }
39
37
  ]
40
38
  }
41
- ]
39
+ )
42
40
  end
43
41
 
44
42
  before do
@@ -47,8 +45,8 @@ describe DatadogBackup::Cli do
47
45
  dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/deleted.json")
48
46
 
49
47
  stubs.get('/api/v1/dashboard') { all_dashboards }
50
- stubs.get('/api/v1/dashboard/stillthere') {[200, {}, {}]}
51
- stubs.get('/api/v1/dashboard/alsostillthere') {[200, {}, {}]}
48
+ stubs.get('/api/v1/dashboard/stillthere') { respond_with200({}) }
49
+ stubs.get('/api/v1/dashboard/alsostillthere') { respond_with200({}) }
52
50
  end
53
51
 
54
52
  it 'deletes the file locally as well' do
@@ -58,66 +56,89 @@ describe DatadogBackup::Cli do
58
56
  end
59
57
  end
60
58
 
61
- describe '#diffs' do
62
- subject { cli.diffs }
63
-
64
- before do
65
- dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs1.json")
66
- dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs2.json")
67
- dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs3.json")
68
- allow(dashboards).to receive(:get_by_id).and_return({ 'text' => 'diff2' })
69
- end
70
-
71
- it {
72
- expect(subject).to include(
73
- " ---\n id: diffs1\n ---\n-text: diff2\n+text: diff\n",
74
- " ---\n id: diffs3\n ---\n-text: diff2\n+text: diff\n",
75
- " ---\n id: diffs2\n ---\n-text: diff2\n+text: diff\n"
76
- )
77
- }
78
- end
79
-
80
59
  describe '#restore' do
81
- subject { cli.restore }
60
+ subject(:restore) { cli.restore }
61
+ let(:stdin) { class_double('STDIN') }
82
62
 
63
+ after(:all) do
64
+ $stdin = STDIN
65
+ end
66
+
83
67
  before do
68
+ $stdin = stdin
84
69
  dashboards.write_file('{"text": "diff"}', "#{tempdir}/dashboards/diffs1.json")
85
70
  allow(dashboards).to receive(:get_by_id).and_return({ 'text' => 'diff2' })
71
+ allow(dashboards).to receive(:write_file)
72
+ allow(dashboards).to receive(:update)
86
73
  end
87
74
 
88
75
  example 'starts interactive restore' do
89
- allow($stdin).to receive(:gets).and_return('q')
76
+ allow(stdin).to receive(:gets).and_return('q')
90
77
 
91
- expect { subject }.to(
78
+ expect { restore }.to(
92
79
  output(/\(r\)estore to Datadog, overwrite local changes and \(d\)ownload, \(s\)kip, or \(q\)uit\?/).to_stdout
93
80
  .and(raise_error(SystemExit))
94
81
  )
95
82
  end
96
83
 
97
- example 'restore' do
98
- allow($stdin).to receive(:gets).and_return('r')
99
- expect(dashboards).to receive(:update).with('diffs1', { 'text' => 'diff' })
100
- subject
84
+ context 'when the user chooses to restore' do
85
+ before do
86
+ allow(stdin).to receive(:gets).and_return('r')
87
+ end
88
+
89
+ example 'it restores from disk to server' do
90
+ restore
91
+ expect(dashboards).to have_received(:update).with('diffs1', { 'text' => 'diff' })
92
+ end
101
93
  end
102
94
 
103
- example 'download' do
104
- allow($stdin).to receive(:gets).and_return('d')
105
- expect(dashboards).to receive(:write_file).with(%({\n "text": "diff2"\n}), "#{tempdir}/dashboards/diffs1.json")
106
- subject
95
+ context 'when the user chooses to download' do
96
+ before do
97
+ allow(stdin).to receive(:gets).and_return('d')
98
+ end
99
+
100
+ example 'it writes from server to disk' do
101
+ restore
102
+ expect(dashboards).to have_received(:write_file).with(%({\n "text": "diff2"\n}), "#{tempdir}/dashboards/diffs1.json")
103
+ end
107
104
  end
108
105
 
109
- example 'skip' do
110
- allow($stdin).to receive(:gets).and_return('s')
111
- expect(dashboards).not_to receive(:write_file)
112
- expect(dashboards).not_to receive(:update)
113
- subject
106
+ context 'when the user chooses to skip' do
107
+ before do
108
+ allow(stdin).to receive(:gets).and_return('s')
109
+ end
110
+
111
+ example 'it does not write to disk' do
112
+ restore
113
+ expect(dashboards).not_to have_received(:write_file)
114
+ end
115
+
116
+ example 'it does not update the server' do
117
+ restore
118
+ expect(dashboards).not_to have_received(:update)
119
+ end
114
120
  end
115
121
 
116
- example 'quit' do
117
- allow($stdin).to receive(:gets).and_return('q')
118
- expect(dashboards).not_to receive(:write_file)
119
- expect(dashboards).not_to receive(:update)
120
- expect { subject }.to raise_error(SystemExit)
122
+ context 'when the user chooses to quit' do
123
+ before do
124
+ allow(stdin).to receive(:gets).and_return('q')
125
+ end
126
+
127
+ example 'it exits' do
128
+ expect { restore }.to raise_error(SystemExit)
129
+ end
130
+
131
+ example 'it does not write to disk' do
132
+ restore
133
+ rescue SystemExit
134
+ expect(dashboards).not_to have_received(:write_file)
135
+ end
136
+
137
+ example 'it does not update the server' do
138
+ restore
139
+ rescue SystemExit
140
+ expect(dashboards).not_to have_received(:update)
141
+ end
121
142
  end
122
143
  end
123
144
  end
@@ -2,133 +2,154 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- describe DatadogBackup::Core do
5
+ describe DatadogBackup::Resources do
6
6
  let(:stubs) { Faraday::Adapter::Test::Stubs.new }
7
7
  let(:api_client_double) { Faraday.new { |f| f.adapter :test, stubs } }
8
8
  let(:tempdir) { Dir.mktmpdir }
9
- let(:core) do
10
- core = described_class.new(
9
+ let(:resources) do
10
+ resources = described_class.new(
11
11
  action: 'backup',
12
12
  backup_dir: tempdir,
13
13
  diff_format: nil,
14
14
  resources: [],
15
15
  output_format: :json
16
16
  )
17
- allow(core).to receive(:api_service).and_return(api_client_double)
18
- return core
17
+ allow(resources).to receive(:api_service).and_return(api_client_double)
18
+ return resources
19
19
  end
20
20
 
21
-
22
-
23
-
24
21
  describe '#diff' do
25
- subject { core.diff('diff') }
22
+ subject(:diff) { resources.diff('diff') }
26
23
 
27
24
  before do
28
- allow(core).to receive(:get_by_id).and_return({ 'text' => 'diff1', 'extra' => 'diff1' })
29
- core.write_file('{"text": "diff2", "extra": "diff2"}', "#{tempdir}/core/diff.json")
25
+ allow(resources).to receive(:get_by_id).and_return({ 'text' => 'diff1', 'extra' => 'diff1' })
26
+ resources.write_file('{"text": "diff2", "extra": "diff2"}', "#{tempdir}/resources/diff.json")
30
27
  end
31
28
 
32
29
  it {
33
- expect(subject).to eq <<~EOF
30
+ expect(diff).to eq(<<~EODIFF
34
31
  ---
35
32
  -extra: diff1
36
33
  -text: diff1
37
34
  +extra: diff2
38
35
  +text: diff2
39
- EOF
36
+ EODIFF
37
+ .chomp)
40
38
  }
41
39
  end
42
40
 
43
41
  describe '#except' do
44
- subject { core.except({ a: :b, b: :c }) }
42
+ subject { resources.except({ a: :b, b: :c }) }
45
43
 
46
44
  it { is_expected.to eq({ a: :b, b: :c }) }
47
45
  end
48
46
 
49
47
  describe '#initialize' do
50
- subject { core }
48
+ subject(:myresources) { resources }
51
49
 
52
50
  it 'makes the subdirectories' do
53
- expect(FileUtils).to receive(:mkdir_p).with("#{tempdir}/core")
54
- subject
51
+ fileutils = class_double(FileUtils).as_stubbed_const
52
+ allow(fileutils).to receive(:mkdir_p)
53
+ myresources
54
+ expect(fileutils).to have_received(:mkdir_p).with("#{tempdir}/resources")
55
55
  end
56
56
  end
57
57
 
58
58
  describe '#myclass' do
59
- subject { core.myclass }
59
+ subject { resources.myclass }
60
60
 
61
- it { is_expected.to eq 'core' }
61
+ it { is_expected.to eq 'resources' }
62
62
  end
63
63
 
64
64
  describe '#create' do
65
- subject { core.create({ 'a' => 'b' }) }
65
+ subject(:create) { resources.create({ 'a' => 'b' }) }
66
66
 
67
67
  example 'it will post /api/v1/dashboard' do
68
- allow(core).to receive(:api_version).and_return('v1')
69
- allow(core).to receive(:api_resource_name).and_return('dashboard')
70
- stubs.post('/api/v1/dashboard', {'a' => 'b'}) {[200, {}, {'id' => 'whatever-id-abc' }]}
71
- subject
68
+ allow(resources).to receive(:api_version).and_return('v1')
69
+ allow(resources).to receive(:api_resource_name).and_return('dashboard')
70
+ stubs.post('/api/v1/dashboard', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) }
71
+ create
72
72
  stubs.verify_stubbed_calls
73
73
  end
74
74
  end
75
75
 
76
76
  describe '#update' do
77
- subject { core.update('abc-123-def', { 'a' => 'b' }) }
77
+ subject(:update) { resources.update('abc-123-def', { 'a' => 'b' }) }
78
78
 
79
79
  example 'it puts /api/v1/dashboard' do
80
- allow(core).to receive(:api_version).and_return('v1')
81
- allow(core).to receive(:api_resource_name).and_return('dashboard')
82
- stubs.put('/api/v1/dashboard/abc-123-def', {'a' => 'b'}) {[200, {}, {'id' => 'whatever-id-abc' }]}
83
- subject
80
+ allow(resources).to receive(:api_version).and_return('v1')
81
+ allow(resources).to receive(:api_resource_name).and_return('dashboard')
82
+ stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { respond_with200({ 'id' => 'whatever-id-abc' }) }
83
+ update
84
84
  stubs.verify_stubbed_calls
85
85
  end
86
86
 
87
87
  context 'when the id is not found' do
88
88
  before do
89
- allow(core).to receive(:api_version).and_return('v1')
90
- allow(core).to receive(:api_resource_name).and_return('dashboard')
91
- stubs.put('/api/v1/dashboard/abc-123-def', {'a' => 'b'}) {[404, {}, {'id' => 'whatever-id-abc' }]}
89
+ allow(resources).to receive(:api_version).and_return('v1')
90
+ allow(resources).to receive(:api_resource_name).and_return('dashboard')
91
+ stubs.put('/api/v1/dashboard/abc-123-def', { 'a' => 'b' }) { [404, {}, { 'id' => 'whatever-id-abc' }] }
92
92
  end
93
+
93
94
  it 'raises an error' do
94
- expect { subject }.to raise_error(RuntimeError, 'update failed with error 404')
95
+ expect { update }.to raise_error(RuntimeError, 'update failed with error 404')
95
96
  end
96
97
  end
97
98
  end
98
99
 
99
100
  describe '#restore' do
100
101
  before do
101
- allow(core).to receive(:api_version).and_return('api-version-string')
102
- allow(core).to receive(:api_resource_name).and_return('api-resource-name-string')
103
- stubs.get('/api/api-version-string/api-resource-name-string/abc-123-def') {[200, {}, {'test' => 'ok' }]}
104
- stubs.get('/api/api-version-string/api-resource-name-string/bad-123-id') {[404, {}, {'error' => 'blahblah_not_found' }]}
105
- allow(core).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' })
102
+ allow(resources).to receive(:api_version).and_return('api-version-string')
103
+ allow(resources).to receive(:api_resource_name).and_return('api-resource-name-string')
104
+ stubs.get('/api/api-version-string/api-resource-name-string/abc-123-def') { respond_with200({ 'test' => 'ok' }) }
105
+ stubs.get('/api/api-version-string/api-resource-name-string/bad-123-id') do
106
+ [404, {}, { 'error' => 'blahblah_not_found' }]
107
+ end
108
+ allow(resources).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' })
106
109
  end
107
110
 
108
111
  context 'when id exists' do
109
- subject { core.restore('abc-123-def') }
112
+ subject(:restore) { resources.restore('abc-123-def') }
110
113
 
111
114
  example 'it calls out to update' do
112
- expect(core).to receive(:update).with('abc-123-def', { 'load' => 'ok' })
113
- subject
115
+ allow(resources).to receive(:update)
116
+ restore
117
+ expect(resources).to have_received(:update).with('abc-123-def', { 'load' => 'ok' })
114
118
  end
115
119
  end
116
120
 
117
121
  context 'when id does not exist on remote' do
118
- subject { core.restore('bad-123-id') }
122
+ subject(:restore_newly) { resources.restore('bad-123-id') }
123
+
124
+ let(:fileutils) { class_double(FileUtils).as_stubbed_const }
119
125
 
120
126
  before do
121
- allow(core).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' })
122
- stubs.put('/api/api-version-string/api-resource-name-string/bad-123-id') {[404, {}, {'error' => 'id not found' }]}
123
- stubs.post('/api/api-version-string/api-resource-name-string', {'load' => 'ok'}) {[200, {}, {'id' => 'my-new-id' }]}
127
+ allow(resources).to receive(:load_from_file_by_id).and_return({ 'load' => 'ok' })
128
+ stubs.put('/api/api-version-string/api-resource-name-string/bad-123-id') do
129
+ [404, {}, { 'error' => 'id not found' }]
130
+ end
131
+ stubs.post('/api/api-version-string/api-resource-name-string', { 'load' => 'ok' }) do
132
+ respond_with200({ 'id' => 'my-new-id' })
133
+ end
134
+ allow(fileutils).to receive(:rm)
135
+ allow(resources).to receive(:create).with({ 'load' => 'ok' }).and_return({ 'id' => 'my-new-id' })
136
+ allow(resources).to receive(:get_and_write_file)
137
+ allow(resources).to receive(:find_file_by_id).with('bad-123-id').and_return('/path/to/bad-123-id.json')
138
+ end
139
+
140
+ example 'it calls out to create' do
141
+ restore_newly
142
+ expect(resources).to have_received(:create).with({ 'load' => 'ok' })
143
+ end
144
+
145
+ example 'it saves the new file' do
146
+ restore_newly
147
+ expect(resources).to have_received(:get_and_write_file).with('my-new-id')
124
148
  end
125
149
 
126
- example 'it calls out to create then saves the new file and deletes the new file' do
127
- expect(core).to receive(:create).with({ 'load' => 'ok' }).and_return({ 'id' => 'my-new-id' })
128
- expect(core).to receive(:get_and_write_file).with('my-new-id')
129
- allow(core).to receive(:find_file_by_id).with('bad-123-id').and_return('/path/to/bad-123-id.json')
130
- expect(FileUtils).to receive(:rm).with('/path/to/bad-123-id.json')
131
- subject
150
+ example 'it deletes the old file' do
151
+ restore_newly
152
+ expect(fileutils).to have_received(:rm).with('/path/to/bad-123-id.json')
132
153
  end
133
154
  end
134
155
  end