journaled 2.0.0.alpha1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +95 -25
  3. data/app/models/concerns/journaled/changes.rb +41 -0
  4. data/app/models/journaled/actor_uri_provider.rb +22 -0
  5. data/app/models/journaled/change_definition.rb +17 -1
  6. data/app/models/journaled/change_writer.rb +3 -22
  7. data/app/models/journaled/event.rb +0 -4
  8. data/config/initializers/change_protection.rb +3 -0
  9. data/lib/journaled.rb +9 -1
  10. data/lib/journaled/relation_change_protection.rb +27 -0
  11. data/lib/journaled/rspec.rb +18 -0
  12. data/lib/journaled/version.rb +1 -1
  13. data/spec/dummy/README.rdoc +28 -0
  14. data/spec/dummy/Rakefile +6 -0
  15. data/spec/dummy/bin/bundle +3 -0
  16. data/spec/dummy/bin/rails +4 -0
  17. data/spec/dummy/bin/rake +4 -0
  18. data/spec/dummy/config.ru +4 -0
  19. data/spec/dummy/config/application.rb +26 -0
  20. data/spec/dummy/config/boot.rb +5 -0
  21. data/spec/dummy/config/database.yml +21 -0
  22. data/spec/dummy/config/environment.rb +5 -0
  23. data/spec/dummy/config/environments/development.rb +37 -0
  24. data/spec/dummy/config/environments/production.rb +78 -0
  25. data/spec/dummy/config/environments/test.rb +39 -0
  26. data/spec/dummy/config/initializers/assets.rb +8 -0
  27. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  28. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  29. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  30. data/spec/dummy/config/initializers/inflections.rb +16 -0
  31. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  32. data/spec/dummy/config/initializers/session_store.rb +3 -0
  33. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  34. data/spec/dummy/config/locales/en.yml +23 -0
  35. data/spec/dummy/config/routes.rb +56 -0
  36. data/spec/dummy/config/secrets.yml +22 -0
  37. data/spec/dummy/db/migrate/20180606205114_create_delayed_jobs.rb +18 -0
  38. data/spec/dummy/db/schema.rb +31 -0
  39. data/spec/dummy/log/development.log +34 -0
  40. data/spec/dummy/log/test.log +34540 -0
  41. data/spec/dummy/public/404.html +67 -0
  42. data/spec/dummy/public/422.html +67 -0
  43. data/spec/dummy/public/500.html +66 -0
  44. data/spec/dummy/public/favicon.ico +0 -0
  45. data/spec/lib/journaled_spec.rb +51 -0
  46. data/spec/models/concerns/journaled/actor_spec.rb +46 -0
  47. data/spec/models/concerns/journaled/changes_spec.rb +94 -0
  48. data/spec/models/database_change_protection_spec.rb +106 -0
  49. data/spec/models/journaled/actor_uri_provider_spec.rb +41 -0
  50. data/spec/models/journaled/change_writer_spec.rb +276 -0
  51. data/spec/models/journaled/delivery_spec.rb +156 -0
  52. data/spec/models/journaled/event_spec.rb +145 -0
  53. data/spec/models/journaled/json_schema_model/validator_spec.rb +133 -0
  54. data/spec/models/journaled/writer_spec.rb +129 -0
  55. data/spec/rails_helper.rb +20 -0
  56. data/spec/spec_helper.rb +22 -0
  57. data/spec/support/delayed_job_spec_helper.rb +11 -0
  58. data/spec/support/environment_spec_helper.rb +16 -0
  59. metadata +113 -4
@@ -0,0 +1,145 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Journaled::Event do
4
+ let(:sample_journaled_event_class) do
5
+ SomeClassName = Class.new do
6
+ include Journaled::Event
7
+ end
8
+ end
9
+
10
+ after do
11
+ Object.send(:remove_const, :SomeClassName) if defined?(SomeClassName)
12
+ Object.send(:remove_const, :SomeModule) if defined?(SomeModule)
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
+ it 'creates a Journaled::Writer with this event and journals it' do
25
+ sample_journaled_event.journal!
26
+ expect(Journaled::Writer).to have_received(:new).with(journaled_event: sample_journaled_event)
27
+ expect(mock_journaled_writer).to have_received(:journal!)
28
+ end
29
+ end
30
+
31
+ describe '#journaled_schema_name' do
32
+ it 'returns the underscored version on the class name' do
33
+ expect(sample_journaled_event.journaled_schema_name).to eq 'some_class_name'
34
+ end
35
+
36
+ context 'when the class is modularized' do
37
+ let(:sample_journaled_event_class) do
38
+ SomeModule = Module.new
39
+ SomeModule::SomeClassName = Class.new do
40
+ include Journaled::Event
41
+ end
42
+ end
43
+
44
+ it 'returns the underscored version on the class name' do
45
+ expect(sample_journaled_event.journaled_schema_name).to eq 'some_module/some_class_name'
46
+ end
47
+ end
48
+ end
49
+
50
+ describe '#event_type' do
51
+ it 'returns the underscored version on the class name' do
52
+ expect(sample_journaled_event.event_type).to eq 'some_class_name'
53
+ end
54
+
55
+ context 'when the class is modularized' do
56
+ let(:sample_journaled_event_class) do
57
+ SomeModule = Module.new
58
+ SomeModule::SomeClassName = Class.new do
59
+ include Journaled::Event
60
+ end
61
+ end
62
+
63
+ it 'returns the underscored version on the class name, with slashes replaced with underscores' do
64
+ expect(sample_journaled_event.event_type).to eq 'some_module_some_class_name'
65
+ end
66
+ end
67
+ end
68
+
69
+ describe '#journaled_partition_key' do
70
+ it 'returns the #event_type' do
71
+ expect(sample_journaled_event.journaled_partition_key).to eq 'some_class_name'
72
+ end
73
+ end
74
+
75
+ describe '#journaled_app_name' do
76
+ it 'returns nil in the base class so it can be set explicitly in apps spanning multiple app domains' do
77
+ expect(sample_journaled_event.journaled_app_name).to be_nil
78
+ end
79
+
80
+ it 'returns the journaled default if set' do
81
+ allow(Journaled).to receive(:default_app_name).and_return("my_app")
82
+ expect(sample_journaled_event.journaled_app_name).to eq("my_app")
83
+ end
84
+ end
85
+
86
+ describe '#journaled_attributes' do
87
+ let(:fake_uuid) { 'FAKE_UUID' }
88
+ let(:frozen_time) { Time.zone.parse('15/2/2017 13:00') }
89
+ before do
90
+ allow(SecureRandom).to receive(:uuid).and_return(fake_uuid).once
91
+ end
92
+ around do |example|
93
+ Timecop.freeze(frozen_time) { example.run }
94
+ end
95
+
96
+ context 'when no additional attributes have been defined' do
97
+ it 'returns the base attributes, and memoizes them after the first call' do
98
+ expect(sample_journaled_event.journaled_attributes).to eq id: fake_uuid, created_at: frozen_time, event_type: 'some_class_name'
99
+ expect(sample_journaled_event.journaled_attributes).to eq id: fake_uuid, created_at: frozen_time, event_type: 'some_class_name'
100
+ end
101
+ end
102
+
103
+ context 'when there are additional attributes specified, but not defined' do
104
+ let(:sample_journaled_event_class) do
105
+ SomeClassName = Class.new do
106
+ include Journaled::Event
107
+
108
+ journal_attributes :foo
109
+ end
110
+ end
111
+
112
+ it 'raises a no method error' do
113
+ expect { sample_journaled_event.journaled_attributes }.to raise_error NoMethodError
114
+ end
115
+ end
116
+
117
+ context 'when there are additional attributes specified and defined' do
118
+ let(:sample_journaled_event_class) do
119
+ SomeClassName = Class.new do
120
+ include Journaled::Event
121
+
122
+ journal_attributes :foo, :bar
123
+
124
+ def foo
125
+ 'foo_return'
126
+ end
127
+
128
+ def bar
129
+ 'bar_return'
130
+ end
131
+ end
132
+ end
133
+
134
+ it 'returns the specified attributes plus the base ones' do
135
+ expect(sample_journaled_event.journaled_attributes).to eq(
136
+ id: fake_uuid,
137
+ created_at: frozen_time,
138
+ event_type: 'some_class_name',
139
+ foo: 'foo_return',
140
+ bar: 'bar_return'
141
+ )
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,133 @@
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
@@ -0,0 +1,129 @@
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_app_name: nil
22
+ )
23
+ end
24
+
25
+ it 'raises on initialization' do
26
+ expect { subject }.to raise_error RuntimeError, /An enqueued event must have a non-nil response to/
27
+ end
28
+ end
29
+
30
+ context 'when the Journaled Event complies with the API' do
31
+ let(:journaled_event) do
32
+ double(
33
+ journaled_schema_name: :fake_schema_name,
34
+ journaled_attributes: { foo: :bar },
35
+ journaled_partition_key: 'fake_partition_key',
36
+ journaled_app_name: nil
37
+ )
38
+ end
39
+
40
+ it 'does not raise on initialization' do
41
+ expect { subject }.to_not raise_error
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#journal!' do
47
+ let(:schema_path) { Journaled::Engine.root.join "journaled_schemas/fake_schema_name.json" }
48
+ let(:schema_file_contents) do
49
+ <<-JSON
50
+ {
51
+ "title": "Foo",
52
+ "type": "object",
53
+ "properties": {
54
+ "foo": {
55
+ "type": "string"
56
+ }
57
+ },
58
+ "required": ["foo"]
59
+ }
60
+ JSON
61
+ end
62
+
63
+ before do
64
+ allow(File).to receive(:exist?).and_call_original
65
+ allow(File).to receive(:exist?).with(schema_path).and_return(true)
66
+ allow(File).to receive(:read).and_call_original
67
+ allow(File).to receive(:read).with(schema_path).and_return(schema_file_contents)
68
+ end
69
+
70
+ let(:journaled_event) do
71
+ double(
72
+ journaled_schema_name: :fake_schema_name,
73
+ journaled_attributes: journaled_event_attributes,
74
+ journaled_partition_key: 'fake_partition_key',
75
+ journaled_app_name: 'my_app'
76
+ )
77
+ end
78
+
79
+ around do |example|
80
+ with_jobs_delayed { example.run }
81
+ end
82
+
83
+ context 'when the journaled event does NOT comply with the base_event schema' do
84
+ let(:journaled_event_attributes) { { foo: 1 } }
85
+
86
+ it 'raises an error and does not enqueue anything' do
87
+ expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
88
+ expect(Delayed::Job.where('handler like ?', '%Journaled::Delivery%').count).to eq 0
89
+ end
90
+ end
91
+
92
+ context 'when the event complies with the base_event schema' do
93
+ context 'when the specific json schema is NOT valid' do
94
+ let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: 1 } }
95
+
96
+ it 'raises an error and does not enqueue anything' do
97
+ expect { subject.journal! }.to raise_error JSON::Schema::ValidationError
98
+ expect(Delayed::Job.where('handler like ?', '%Journaled::Delivery%').count).to eq 0
99
+ end
100
+ end
101
+
102
+ context 'when the specific json schema is also valid' do
103
+ let(:journaled_event_attributes) { { id: 'FAKE_UUID', event_type: 'fake_event', created_at: Time.zone.now, foo: :bar } }
104
+
105
+ it 'creates a delivery with the app name passed through' do
106
+ allow(Journaled::Delivery).to receive(:new).and_call_original
107
+ subject.journal!
108
+ expect(Journaled::Delivery).to have_received(:new).with(hash_including(app_name: 'my_app'))
109
+ end
110
+
111
+ it 'enqueues a Journaled::Delivery object with the serialized journaled_event at the lowest priority' do
112
+ expect { subject.journal! }.to change {
113
+ Delayed::Job.where('handler like ?', '%Journaled::Delivery%').where(priority: Journaled::JobPriority::EVENTUAL).count
114
+ }.from(0).to(1)
115
+ end
116
+
117
+ context 'when the Writer was initialized with a priority' do
118
+ subject { described_class.new journaled_event: journaled_event, priority: Journaled::JobPriority::INTERACTIVE }
119
+
120
+ it 'enqueues the event at the given priority' do
121
+ expect { subject.journal! }.to change {
122
+ Delayed::Job.where('handler like ?', '%Journaled::Delivery%').where(priority: Journaled::JobPriority::INTERACTIVE).count
123
+ }.from(0).to(1)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,20 @@
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
+ require 'pry-rails'
10
+
11
+ Dir[Rails.root.join('..', 'support', '**', '*.rb')].each { |f| require f }
12
+
13
+ RSpec.configure do |config|
14
+ config.use_transactional_fixtures = true
15
+
16
+ config.infer_spec_type_from_file_location!
17
+
18
+ config.include DelayedJobSpecHelper
19
+ config.include EnvironmentSpecHelper
20
+ end
@@ -0,0 +1,22 @@
1
+ require 'delayed_job_active_record'
2
+ rails_env = ENV['RAILS_ENV'] ||= 'test'
3
+ db_adapter = ENV['DB_ADAPTER'] ||= 'postgresql'
4
+ require File.expand_path('dummy/config/environment.rb', __dir__)
5
+
6
+ Rails.configuration.database_configuration[db_adapter][rails_env].tap do |c|
7
+ ActiveRecord::Tasks::DatabaseTasks.create(c)
8
+ ActiveRecord::Base.establish_connection(c)
9
+ load File.expand_path('dummy/db/schema.rb', __dir__)
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ config.expect_with :rspec do |expectations|
14
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
15
+ end
16
+
17
+ config.mock_with :rspec do |mocks|
18
+ mocks.verify_partial_doubles = true
19
+ end
20
+
21
+ config.order = :random
22
+ end