journaled 2.0.0.alpha1 → 2.0.0.rc1

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 (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