datadog_backup 3.0.0.alpha.2 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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