journaled 4.1.0 → 5.0.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -46
  3. data/Rakefile +10 -24
  4. data/app/jobs/journaled/delivery_job.rb +17 -28
  5. data/app/models/concerns/journaled/changes.rb +5 -5
  6. data/app/models/journaled/change.rb +12 -12
  7. data/app/models/journaled/change_writer.rb +3 -2
  8. data/app/models/journaled/event.rb +1 -1
  9. data/app/models/journaled/writer.rb +32 -15
  10. data/lib/journaled/connection.rb +48 -0
  11. data/lib/journaled/engine.rb +5 -0
  12. data/lib/journaled/errors.rb +3 -0
  13. data/lib/journaled/relation_change_protection.rb +11 -10
  14. data/lib/journaled/rspec.rb +86 -0
  15. data/lib/journaled/transaction_ext.rb +31 -0
  16. data/lib/journaled/version.rb +1 -1
  17. data/lib/journaled.rb +17 -13
  18. metadata +54 -97
  19. data/spec/dummy/README.rdoc +0 -28
  20. data/spec/dummy/Rakefile +0 -6
  21. data/spec/dummy/bin/bundle +0 -3
  22. data/spec/dummy/bin/rails +0 -4
  23. data/spec/dummy/bin/rake +0 -4
  24. data/spec/dummy/config/application.rb +0 -25
  25. data/spec/dummy/config/boot.rb +0 -5
  26. data/spec/dummy/config/database.yml +0 -6
  27. data/spec/dummy/config/environment.rb +0 -5
  28. data/spec/dummy/config/environments/development.rb +0 -24
  29. data/spec/dummy/config/environments/test.rb +0 -37
  30. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  31. data/spec/dummy/config/initializers/cookies_serializer.rb +0 -3
  32. data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  33. data/spec/dummy/config/initializers/inflections.rb +0 -16
  34. data/spec/dummy/config/initializers/mime_types.rb +0 -4
  35. data/spec/dummy/config/initializers/session_store.rb +0 -3
  36. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  37. data/spec/dummy/config/locales/en.yml +0 -23
  38. data/spec/dummy/config/routes.rb +0 -56
  39. data/spec/dummy/config/secrets.yml +0 -22
  40. data/spec/dummy/config.ru +0 -4
  41. data/spec/dummy/db/schema.rb +0 -18
  42. data/spec/dummy/public/404.html +0 -67
  43. data/spec/dummy/public/422.html +0 -67
  44. data/spec/dummy/public/500.html +0 -66
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/jobs/journaled/delivery_job_spec.rb +0 -276
  47. data/spec/lib/journaled_spec.rb +0 -91
  48. data/spec/models/concerns/journaled/actor_spec.rb +0 -47
  49. data/spec/models/concerns/journaled/changes_spec.rb +0 -106
  50. data/spec/models/database_change_protection_spec.rb +0 -109
  51. data/spec/models/journaled/actor_uri_provider_spec.rb +0 -42
  52. data/spec/models/journaled/change_writer_spec.rb +0 -281
  53. data/spec/models/journaled/event_spec.rb +0 -236
  54. data/spec/models/journaled/json_schema_model/validator_spec.rb +0 -133
  55. data/spec/models/journaled/writer_spec.rb +0 -174
  56. data/spec/rails_helper.rb +0 -19
  57. data/spec/spec_helper.rb +0 -20
  58. data/spec/support/environment_spec_helper.rb +0 -16
@@ -1,236 +0,0 @@
1
- require 'rails_helper'
2
-
3
- RSpec.describe Journaled::Event do
4
- let(:sample_journaled_event_class_name) { 'SomeClassName' }
5
- let(:sample_journaled_event_class) do
6
- Class.new do
7
- include Journaled::Event
8
- end
9
- end
10
-
11
- before do
12
- stub_const(sample_journaled_event_class_name, sample_journaled_event_class)
13
- end
14
-
15
- let(:sample_journaled_event) { sample_journaled_event_class.new }
16
-
17
- describe '#journal!' do
18
- let(:mock_journaled_writer) { instance_double(Journaled::Writer, journal!: nil) }
19
-
20
- before do
21
- allow(Journaled::Writer).to receive(:new).and_return(mock_journaled_writer)
22
- end
23
-
24
- context 'when no app job priority is set' do
25
- it 'creates a Journaled::Writer with this event and journals it with the default priority' do
26
- sample_journaled_event.journal!
27
- expect(Journaled::Writer).to have_received(:new)
28
- .with(journaled_event: sample_journaled_event)
29
- expect(mock_journaled_writer).to have_received(:journal!)
30
- end
31
- end
32
- end
33
-
34
- describe '#journaled_schema_name' do
35
- it 'returns the underscored version on the class name' do
36
- expect(sample_journaled_event.journaled_schema_name).to eq 'some_class_name'
37
- end
38
-
39
- context 'when the class is modularized' do
40
- let(:sample_journaled_event_class_name) { 'SomeModule::SomeClassName' }
41
-
42
- it 'returns the underscored version on the class name' do
43
- expect(sample_journaled_event.journaled_schema_name).to eq 'some_module/some_class_name'
44
- end
45
- end
46
- end
47
-
48
- describe '#event_type' do
49
- it 'returns the underscored version on the class name' do
50
- expect(sample_journaled_event.event_type).to eq 'some_class_name'
51
- end
52
-
53
- context 'when the class is modularized' do
54
- let(:sample_journaled_event_class_name) { 'SomeModule::SomeClassName' }
55
-
56
- it 'returns the underscored version on the class name, with slashes replaced with underscores' do
57
- expect(sample_journaled_event.event_type).to eq 'some_module_some_class_name'
58
- end
59
- end
60
- end
61
-
62
- describe '#journaled_partition_key' do
63
- it 'returns the #event_type' do
64
- expect(sample_journaled_event.journaled_partition_key).to eq 'some_class_name'
65
- end
66
- end
67
-
68
- describe '#journaled_stream_name' do
69
- it 'returns nil in the base class so it can be set explicitly in apps spanning multiple app domains' do
70
- expect(sample_journaled_event.journaled_stream_name).to be_nil
71
- end
72
-
73
- it 'returns the journaled default if set' do
74
- allow(Journaled).to receive(:default_stream_name).and_return("my_app_events")
75
- expect(sample_journaled_event.journaled_stream_name).to eq("my_app_events")
76
- end
77
- end
78
-
79
- describe '#journaled_attributes' do
80
- let(:fake_uuid) { 'FAKE_UUID' }
81
- let(:frozen_time) { Time.zone.parse('15/2/2017 13:00') }
82
- before do
83
- allow(SecureRandom).to receive(:uuid).and_return(fake_uuid).once
84
- end
85
- around do |example|
86
- Timecop.freeze(frozen_time) { example.run }
87
- end
88
-
89
- context 'when no additional attributes have been defined' do
90
- it 'returns the base attributes, and memoizes them after the first call' do
91
- expect(sample_journaled_event.journaled_attributes)
92
- .to eq id: fake_uuid, created_at: frozen_time, event_type: 'some_class_name'
93
- expect(sample_journaled_event.journaled_attributes)
94
- .to eq id: fake_uuid, created_at: frozen_time, event_type: 'some_class_name'
95
- end
96
- end
97
-
98
- context 'when there are additional attributes specified, but not defined' do
99
- let(:sample_journaled_event_class) do
100
- Class.new do
101
- include Journaled::Event
102
-
103
- journal_attributes :foo
104
- end
105
- end
106
-
107
- it 'raises a no method error' do
108
- expect { sample_journaled_event.journaled_attributes }.to raise_error NoMethodError
109
- end
110
- end
111
-
112
- context 'when there are additional attributes specified and defined' do
113
- let(:sample_journaled_event_class) do
114
- Class.new do
115
- include Journaled::Event
116
-
117
- journal_attributes :foo, :bar
118
-
119
- def foo
120
- 'foo_return'
121
- end
122
-
123
- def bar
124
- 'bar_return'
125
- end
126
- end
127
- end
128
-
129
- it 'returns the specified attributes plus the base ones' do
130
- expect(sample_journaled_event.journaled_attributes).to eq(
131
- id: fake_uuid,
132
- created_at: frozen_time,
133
- event_type: 'some_class_name',
134
- foo: 'foo_return',
135
- bar: 'bar_return',
136
- )
137
- end
138
- end
139
-
140
- context 'tagged: true' do
141
- before do
142
- sample_journaled_event_class.journal_attributes tagged: true
143
- end
144
-
145
- it 'adds a "tags" attribute' do
146
- expect(sample_journaled_event.journaled_attributes).to include(tags: {})
147
- end
148
-
149
- context 'when tags are specified' do
150
- around do |example|
151
- Journaled.tag!(foo: 'bar')
152
- Journaled.tagged(baz: 'bat') { example.run }
153
- end
154
-
155
- it 'adds them to the journaled attributes' do
156
- expect(sample_journaled_event.journaled_attributes).to include(
157
- tags: { foo: 'bar', baz: 'bat' },
158
- )
159
- end
160
-
161
- context 'when even more tags are nested' do
162
- it 'merges them in and then resets them' do
163
- Journaled.tagged(oh_no: 'even more tags') do
164
- expect(sample_journaled_event.journaled_attributes).to include(
165
- tags: { foo: 'bar', baz: 'bat', oh_no: 'even more tags' },
166
- )
167
- end
168
-
169
- allow(SecureRandom).to receive(:uuid).and_return(fake_uuid).once
170
- expect(sample_journaled_event_class.new.journaled_attributes).to include(
171
- tags: { foo: 'bar', baz: 'bat' },
172
- )
173
- end
174
- end
175
-
176
- context 'when custom event tags are also specified and merged' do
177
- let(:sample_journaled_event_class) do
178
- Class.new do
179
- include Journaled::Event
180
-
181
- def tags
182
- super.merge(abc: '123')
183
- end
184
- end
185
- end
186
-
187
- it 'combines all tags' do
188
- expect(sample_journaled_event.journaled_attributes).to include(
189
- tags: { foo: 'bar', baz: 'bat', abc: '123' },
190
- )
191
- end
192
- end
193
-
194
- context 'when custom event tags are also specified but not merged' do
195
- let(:sample_journaled_event_class) do
196
- Class.new do
197
- include Journaled::Event
198
-
199
- def tags
200
- { bananas: 'are great', but_not_actually: 'the best source of potassium' } # it's true
201
- end
202
- end
203
- end
204
-
205
- it 'adds them to the journaled attributes' do
206
- expect(sample_journaled_event.journaled_attributes).to include(
207
- tags: { bananas: 'are great', but_not_actually: 'the best source of potassium' },
208
- )
209
- end
210
- end
211
- end
212
- end
213
- end
214
-
215
- describe '#journaled_enqueue_opts, .journaled_enqueue_opts' do
216
- it 'defaults to an empty hash' do
217
- expect(sample_journaled_event.journaled_enqueue_opts).to eq({})
218
- expect(sample_journaled_event_class.journaled_enqueue_opts).to eq({})
219
- end
220
-
221
- context 'when there are custom opts provided' do
222
- let(:sample_journaled_event_class) do
223
- Class.new do
224
- include Journaled::Event
225
-
226
- journal_attributes :foo, enqueue_with: { priority: 34, foo: 'bar' }
227
- end
228
- end
229
-
230
- it 'merges in the custom opts' do
231
- expect(sample_journaled_event.journaled_enqueue_opts).to eq(priority: 34, foo: 'bar')
232
- expect(sample_journaled_event_class.journaled_enqueue_opts).to eq(priority: 34, foo: 'bar')
233
- end
234
- end
235
- end
236
- end
@@ -1,133 +0,0 @@
1
- require 'rails_helper'
2
-
3
- RSpec.describe Journaled::JsonSchemaModel::Validator do
4
- subject { described_class.new schema_name }
5
-
6
- describe '#validate!' do
7
- let(:json_to_validate) { attributes_to_validate.to_json }
8
- let(:attributes_to_validate) do
9
- {
10
- some_string: some_string_value,
11
- some_decimal: 0.1.to_d,
12
- some_time: Time.zone.parse('2017-01-20 15:16:17 -05:00'),
13
- some_int: some_int_value,
14
- some_optional_string: some_optional_string_value,
15
- some_nullable_field: some_nullable_field,
16
- }
17
- end
18
- let(:some_int_value) { 3 }
19
- let(:some_string_value) { 'SOME_STRING' }
20
- let(:some_optional_string_value) { 'SOME_OPTIONAL_STRING' }
21
- let(:some_nullable_field) { 'VALUE' }
22
-
23
- subject { described_class.new schema_name }
24
-
25
- context 'when the schema name matches a schema in journaled' do
26
- let(:schema_name) { :fake_json_schema_name }
27
- let(:gem_path) { Journaled::Engine.root.join "journaled_schemas/#{schema_name}.json" }
28
- let(:schema_path) { Rails.root.join "journaled_schemas", "#{schema_name}.json" }
29
- let(:schema_file_contents) do
30
- <<-JSON
31
- {
32
- "title": "Person",
33
- "type": "object",
34
- "properties": {
35
- "some_string": {
36
- "type": "string"
37
- },
38
- "some_decimal": {
39
- "type": "string"
40
- },
41
- "some_time": {
42
- "type": "string"
43
- },
44
- "some_int": {
45
- "type": "integer"
46
- },
47
- "some_optional_string": {
48
- "type": "string"
49
- },
50
- "some_nullable_field": {
51
- "type": ["string", "null"]
52
- }
53
- },
54
- "required": ["some_string", "some_decimal", "some_time", "some_int", "some_nullable_field"]
55
- }
56
- JSON
57
- end
58
-
59
- before do
60
- allow(File).to receive(:exist?).and_call_original
61
- allow(File).to receive(:exist?).with(gem_path).and_return(false)
62
- allow(File).to receive(:exist?).with(schema_path).and_return(true)
63
- allow(File).to receive(:read).with(schema_path).and_return(schema_file_contents)
64
- end
65
-
66
- context 'when all the required fields are provided' do
67
- context 'when all the fields are provided' do
68
- it 'is valid' do
69
- expect(subject.validate!(json_to_validate)).to eq true
70
- end
71
- end
72
-
73
- context 'when an optional field is missing' do
74
- let(:attributes_to_validate) do
75
- {
76
- some_string: some_string_value,
77
- some_decimal: 0.1.to_d,
78
- some_time: Time.zone.parse('2017-01-20 15:16:17 -05:00'),
79
- some_int: some_int_value,
80
- some_nullable_field: some_nullable_field,
81
- }
82
- end
83
-
84
- it 'is valid' do
85
- expect(subject.validate!(json_to_validate)).to eq true
86
- end
87
- end
88
-
89
- context 'when a nullable field is nil' do
90
- let(:some_nullable_optional_field) { nil }
91
-
92
- it 'is valid' do
93
- expect(subject.validate!(json_to_validate)).to eq true
94
- end
95
- end
96
-
97
- context 'but one of the required fields is of the wrong type' do
98
- let(:some_int_value) { 'NOT_AN_INTEGER' }
99
-
100
- it 'is NOT valid' do
101
- expect { subject.validate! json_to_validate }.to raise_error JSON::Schema::ValidationError
102
- end
103
- end
104
- end
105
-
106
- context 'when not all the required fields are provided' do
107
- let(:attributes_to_validate) do
108
- {
109
- some_string: some_string_value,
110
- some_decimal: 0.1.to_d,
111
- some_time: Time.zone.parse('2017-01-20 15:16:17 -05:00'),
112
- }
113
- end
114
-
115
- it 'is NOT valid' do
116
- expect { subject.validate! json_to_validate }.to raise_error JSON::Schema::ValidationError
117
- end
118
- end
119
- end
120
-
121
- context 'when the schema name does not match a schema in journaled' do
122
- let(:schema_name) { :nonexistent_avro_scehma }
123
-
124
- before do
125
- allow(File).to receive(:exist?).and_return(false)
126
- end
127
-
128
- it 'raises an error loading the schema' do
129
- expect { subject.validate! json_to_validate }.to raise_error(/not found in any of Journaled::Engine.root,/)
130
- end
131
- end
132
- end
133
- end
@@ -1,174 +0,0 @@
1
- require 'rails_helper'
2
-
3
- RSpec.describe Journaled::Writer do
4
- subject { described_class.new journaled_event: journaled_event }
5
-
6
- describe '#initialize' do
7
- context 'when the Journaled Event does not implement all the necessary methods' do
8
- let(:journaled_event) { double }
9
-
10
- it 'raises on initialization' do
11
- expect { subject }.to raise_error RuntimeError, /An enqueued event must respond to/
12
- end
13
- end
14
-
15
- context 'when the Journaled Event returns non-present values for some of the required methods' do
16
- let(:journaled_event) do
17
- double(
18
- journaled_schema_name: nil,
19
- journaled_attributes: {},
20
- journaled_partition_key: '',
21
- journaled_stream_name: nil,
22
- journaled_enqueue_opts: {},
23
- )
24
- end
25
-
26
- it 'raises on initialization' do
27
- expect { subject }.to raise_error RuntimeError, /An enqueued event must have a non-nil response to/
28
- end
29
- end
30
-
31
- context 'when the Journaled Event complies with the API' do
32
- let(:journaled_event) do
33
- double(
34
- journaled_schema_name: :fake_schema_name,
35
- journaled_attributes: { foo: :bar },
36
- journaled_partition_key: 'fake_partition_key',
37
- journaled_stream_name: nil,
38
- journaled_enqueue_opts: {},
39
- )
40
- end
41
-
42
- it 'does not raise on initialization' do
43
- expect { subject }.not_to raise_error
44
- end
45
- end
46
- end
47
-
48
- describe '#journal!' do
49
- let(:schema_path) { Journaled::Engine.root.join "journaled_schemas/fake_schema_name.json" }
50
- let(:schema_file_contents) do
51
- <<-JSON
52
- {
53
- "title": "Foo",
54
- "type": "object",
55
- "properties": {
56
- "foo": {
57
- "type": "string"
58
- }
59
- },
60
- "required": ["foo"]
61
- }
62
- JSON
63
- end
64
-
65
- before do
66
- allow(File).to receive(:exist?).and_call_original
67
- allow(File).to receive(:exist?).with(schema_path).and_return(true)
68
- allow(File).to receive(:read).and_call_original
69
- allow(File).to receive(:read).with(schema_path).and_return(schema_file_contents)
70
- end
71
-
72
- let(:journaled_enqueue_opts) { {} }
73
- let(:journaled_event) do
74
- double(
75
- journaled_schema_name: :fake_schema_name,
76
- journaled_attributes: journaled_event_attributes,
77
- journaled_partition_key: 'fake_partition_key',
78
- journaled_stream_name: 'my_app_events',
79
- journaled_enqueue_opts: journaled_enqueue_opts,
80
- tagged?: false,
81
- )
82
- end
83
-
84
- context 'when the journaled event does NOT comply with the base_event schema' do
85
- let(:journaled_event_attributes) { { foo: 1 } }
86
-
87
- it 'raises an error and does not enqueue anything' do
88
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
89
- expect(enqueued_jobs.count).to eq 0
90
- end
91
- end
92
-
93
- context 'when the event complies with the base_event schema' do
94
- context 'when the specific json schema is NOT valid' do
95
- let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 1 } }
96
-
97
- it 'raises an error and does not enqueue anything' do
98
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
99
- expect(enqueued_jobs.count).to eq 0
100
- end
101
- end
102
-
103
- context 'when the specific json schema is also valid' do
104
- let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: :bar } }
105
-
106
- it 'creates a delivery with the app name passed through' do
107
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
108
- expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
109
- end
110
-
111
- context 'when there is no job priority specified in the enqueue opts' do
112
- around do |example|
113
- old_priority = Journaled.job_priority
114
- Journaled.job_priority = 999
115
- example.run
116
- Journaled.job_priority = old_priority
117
- end
118
-
119
- it 'defaults to the global default' do
120
- expect { subject.journal! }.to change {
121
- if Rails::VERSION::MAJOR < 6
122
- enqueued_jobs.select { |j| j[:job] == Journaled::DeliveryJob }.count
123
- else
124
- enqueued_jobs.select { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 999 }.count
125
- end
126
- }.from(0).to(1)
127
- end
128
- end
129
-
130
- context 'when there is a job priority specified in the enqueue opts' do
131
- let(:journaled_enqueue_opts) { { priority: 13 } }
132
-
133
- it 'enqueues a Journaled::DeliveryJob with the given priority' do
134
- expect { subject.journal! }.to change {
135
- if Rails::VERSION::MAJOR < 6
136
- enqueued_jobs.select { |j| j[:job] == Journaled::DeliveryJob }.count
137
- else
138
- enqueued_jobs.select { |j| j['job_class'] == 'Journaled::DeliveryJob' && j['priority'] == 13 }.count
139
- end
140
- }.from(0).to(1)
141
- end
142
- end
143
- end
144
- end
145
-
146
- context 'when the event is tagged' do
147
- before do
148
- allow(journaled_event).to receive(:tagged?).and_return(true)
149
- end
150
-
151
- context 'and the "tags" attribute is not present' do
152
- let(:journaled_event_attributes) do
153
- { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 'bar' }
154
- end
155
-
156
- it 'raises an error and does not enqueue anything' do
157
- expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
158
- expect(enqueued_jobs.count).to eq 0
159
- end
160
- end
161
-
162
- context 'and the "tags" attribute is present' do
163
- let(:journaled_event_attributes) do
164
- { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 'bar', tags: { baz: 'bat' } }
165
- end
166
-
167
- it 'creates a delivery with the app name passed through' do
168
- expect { subject.journal! }.to change { enqueued_jobs.count }.from(0).to(1)
169
- expect(enqueued_jobs.first[:args].first).to include('stream_name' => 'my_app_events')
170
- end
171
- end
172
- end
173
- end
174
- end
data/spec/rails_helper.rb DELETED
@@ -1,19 +0,0 @@
1
- # This file is copied to spec/ when you run 'rails generate rspec:install'
2
- ENV['RAILS_ENV'] ||= 'test'
3
- require 'spec_helper'
4
- require File.expand_path('../spec/dummy/config/environment', __dir__)
5
- require 'rspec/rails'
6
- require 'timecop'
7
- require 'webmock/rspec'
8
- require 'journaled/rspec'
9
-
10
- Dir[Rails.root.join('..', 'support', '**', '*.rb')].each { |f| require f }
11
-
12
- RSpec.configure do |config|
13
- config.use_transactional_fixtures = true
14
-
15
- config.infer_spec_type_from_file_location!
16
-
17
- config.include ActiveJob::TestHelper
18
- config.include EnvironmentSpecHelper
19
- end
data/spec/spec_helper.rb DELETED
@@ -1,20 +0,0 @@
1
- rails_env = ENV['RAILS_ENV'] ||= 'test'
2
- require File.expand_path('dummy/config/environment.rb', __dir__)
3
-
4
- Rails.configuration.database_configuration[rails_env].tap do |c|
5
- ActiveRecord::Tasks::DatabaseTasks.create(c)
6
- ActiveRecord::Base.establish_connection(c)
7
- load File.expand_path('dummy/db/schema.rb', __dir__)
8
- end
9
-
10
- RSpec.configure do |config|
11
- config.expect_with :rspec do |expectations|
12
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
13
- end
14
-
15
- config.mock_with :rspec do |mocks|
16
- mocks.verify_partial_doubles = true
17
- end
18
-
19
- config.order = :random
20
- end
@@ -1,16 +0,0 @@
1
- module EnvironmentSpecHelper
2
- def with_env(opts = {})
3
- old = {}
4
- opts.each do |k, v|
5
- k = k.to_s
6
- v = v.to_s unless v.nil?
7
- old[k] = ENV[k]
8
- ENV[k] = v
9
- end
10
- yield
11
- ensure
12
- old.each do |k, v|
13
- ENV[k] = v
14
- end
15
- end
16
- end