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,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/404.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The page you were looking for doesn't exist.</h1>
62
+ <p>You may have mistyped the address or the page may have moved.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
@@ -0,0 +1,51 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Journaled do
4
+ it "is enabled in production" do
5
+ allow(Rails).to receive(:env).and_return("production")
6
+ expect(Journaled).to be_enabled
7
+ end
8
+
9
+ it "is disabled in development" do
10
+ allow(Rails).to receive(:env).and_return("development")
11
+ expect(Journaled).not_to be_enabled
12
+ end
13
+
14
+ it "is disabled in test" do
15
+ allow(Rails).to receive(:env).and_return("test")
16
+ expect(Journaled).not_to be_enabled
17
+ end
18
+
19
+ it "is enabled in whatevs" do
20
+ allow(Rails).to receive(:env).and_return("whatevs")
21
+ expect(Journaled).to be_enabled
22
+ end
23
+
24
+ it "is enabled when explicitly enabled in development" do
25
+ with_env(JOURNALED_ENABLED: true) do
26
+ allow(Rails).to receive(:env).and_return("development")
27
+ expect(Journaled).to be_enabled
28
+ end
29
+ end
30
+
31
+ it "is disabled when explicitly disabled in production" do
32
+ with_env(JOURNALED_ENABLED: false) do
33
+ allow(Rails).to receive(:env).and_return("production")
34
+ expect(Journaled).not_to be_enabled
35
+ end
36
+ end
37
+
38
+ it "is disabled when explicitly disabled with empty string" do
39
+ with_env(JOURNALED_ENABLED: '') do
40
+ allow(Rails).to receive(:env).and_return("production")
41
+ expect(Journaled).not_to be_enabled
42
+ end
43
+ end
44
+
45
+ describe "#actor_uri" do
46
+ it "delegates to ActorUriProvider" do
47
+ allow(Journaled::ActorUriProvider).to receive(:instance).and_return(double(actor_uri: "my actor uri"))
48
+ expect(Journaled.actor_uri).to eq "my actor uri"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,46 @@
1
+ require 'rails_helper'
2
+
3
+ # This is a controller mixin, but testing as a model spec!
4
+ RSpec.describe Journaled::Actor do
5
+ let(:user) { double("User") }
6
+ let(:klass) do
7
+ Class.new do
8
+ cattr_accessor :before_actions
9
+ self.before_actions = []
10
+
11
+ def self.before_action(&hook)
12
+ before_actions << hook
13
+ end
14
+
15
+ include Journaled::Actor
16
+
17
+ self.journaled_actor = :current_user
18
+
19
+ def current_user
20
+ nil
21
+ end
22
+
23
+ def trigger_before_actions
24
+ before_actions.each { |proc| instance_eval(&proc) }
25
+ end
26
+ end
27
+ end
28
+
29
+ subject { klass.new }
30
+
31
+ it "Stores a thunk returning nil if current_user returns nil" do
32
+ subject.trigger_before_actions
33
+
34
+ allow(subject).to receive(:current_user).and_return(nil)
35
+
36
+ expect(RequestStore.store[:journaled_actor_proc].call).to eq nil
37
+ end
38
+
39
+ it "Stores a thunk returning current_user if it is set when called" do
40
+ subject.trigger_before_actions
41
+
42
+ allow(subject).to receive(:current_user).and_return(user)
43
+
44
+ expect(RequestStore.store[:journaled_actor_proc].call).to eq user
45
+ end
46
+ end
@@ -0,0 +1,94 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe Journaled::Changes do
4
+ let(:klass) do
5
+ Class.new do
6
+ cattr_accessor :after_create_hooks
7
+ self.after_create_hooks = []
8
+ cattr_accessor :after_save_hooks
9
+ self.after_save_hooks = []
10
+ cattr_accessor :after_destroy_hooks
11
+ self.after_destroy_hooks = []
12
+
13
+ def self.after_create(&hook)
14
+ after_create_hooks << hook
15
+ end
16
+
17
+ def self.after_save(opts, &hook)
18
+ # This is a back-door assertion to prevent regressions in the module's hook definition behavior
19
+ raise "expected `unless: :saved_change_to_id?`" unless opts[:unless] == :saved_change_to_id?
20
+ after_save_hooks << hook
21
+ end
22
+
23
+ def self.after_destroy(&hook)
24
+ after_destroy_hooks << hook
25
+ end
26
+
27
+ include Journaled::Changes
28
+ journal_changes_to :my_heart, as: :change_of_heart
29
+
30
+ def trigger_after_create_hooks
31
+ after_create_hooks.each { |proc| instance_eval(&proc) }
32
+ end
33
+
34
+ def trigger_after_save_hooks
35
+ after_save_hooks.each { |proc| instance_eval(&proc) }
36
+ end
37
+
38
+ def trigger_after_destroy_hooks
39
+ after_destroy_hooks.each { |proc| instance_eval(&proc) }
40
+ end
41
+ end
42
+ end
43
+
44
+ subject { klass.new }
45
+
46
+ let(:change_writer) { double(Journaled::ChangeWriter, create: true, update: true, delete: true) }
47
+
48
+ before do
49
+ allow(Journaled::ChangeWriter).to receive(:new) do |opts|
50
+ expect(opts[:model]).to eq(subject)
51
+ expect(opts[:change_definition].logical_operation).to eq(:change_of_heart)
52
+ change_writer
53
+ end
54
+ end
55
+
56
+ it "can be asserted on with our matcher" do
57
+ expect(klass).to journal_changes_to(:my_heart, as: :change_of_heart)
58
+
59
+ expect(klass).not_to journal_changes_to(:your_heart, as: :something_else)
60
+
61
+ expect {
62
+ expect(klass).to journal_changes_to(:foobaloo, as: :an_event_to_remember)
63
+ }.to raise_error(/> to journal changes to :foobaloo as :an_event_to_remember/)
64
+
65
+ expect {
66
+ expect(klass).not_to journal_changes_to(:my_heart, as: :change_of_heart)
67
+ }.to raise_error(/> not to journal changes to :my_heart as :change_of_heart/)
68
+ end
69
+
70
+ it "has a single change definition" do
71
+ expect(klass._journaled_change_definitions.length).to eq 1
72
+ end
73
+
74
+ it "journals create events on create" do
75
+ subject.trigger_after_create_hooks
76
+
77
+ expect(change_writer).to have_received(:create)
78
+ expect(Journaled::ChangeWriter).to have_received(:new)
79
+ end
80
+
81
+ it "journals update events on save" do
82
+ subject.trigger_after_save_hooks
83
+
84
+ expect(change_writer).to have_received(:update)
85
+ expect(Journaled::ChangeWriter).to have_received(:new)
86
+ end
87
+
88
+ it "journals delete events on destroy" do
89
+ subject.trigger_after_destroy_hooks
90
+
91
+ expect(change_writer).to have_received(:delete)
92
+ expect(Journaled::ChangeWriter).to have_received(:new)
93
+ end
94
+ end
@@ -0,0 +1,106 @@
1
+ require 'rails_helper'
2
+
3
+ # rubocop:disable Rails/SkipsModelValidations
4
+ RSpec.describe "Raw database change protection" do
5
+ let(:journaled_class) do
6
+ Class.new(Delayed::Job) do
7
+ include Journaled::Changes
8
+
9
+ journal_changes_to :locked_at, as: :attempt
10
+ end
11
+ end
12
+
13
+ let(:journaled_class_with_no_journaled_columns) do
14
+ Class.new(Delayed::Job) do
15
+ include Journaled::Changes
16
+ end
17
+ end
18
+
19
+ describe "the relation" do
20
+ describe "#update_all" do
21
+ it "refuses on journaled columns" do
22
+ expect { journaled_class.update_all(locked_at: nil) }.to raise_error(/aborted by Journaled/)
23
+ end
24
+
25
+ it "succeeds on unjournaled columns" do
26
+ expect { journaled_class.update_all(handler: "") }.not_to raise_error
27
+ end
28
+
29
+ it "succeeds when forced on journaled columns" do
30
+ expect { journaled_class.update_all({ locked_at: nil }, force: true) }.not_to raise_error
31
+ end
32
+ end
33
+
34
+ describe "#delete" do
35
+ it "refuses if journaled columns exist" do
36
+ expect { journaled_class.delete(1) }.to raise_error(/aborted by Journaled/)
37
+ end
38
+
39
+ it "succeeds if no journaled columns exist" do
40
+ expect { journaled_class_with_no_journaled_columns.delete(1) }.not_to raise_error
41
+ end
42
+
43
+ it "succeeds if journaled columns exist when forced" do
44
+ expect { journaled_class.delete(1, force: true) }.not_to raise_error
45
+ end
46
+ end
47
+
48
+ describe "#delete_all" do
49
+ it "refuses if journaled columns exist" do
50
+ expect { journaled_class.delete_all }.to raise_error(/aborted by Journaled/)
51
+ end
52
+
53
+ it "succeeds if no journaled columns exist" do
54
+ expect { journaled_class_with_no_journaled_columns.delete_all }.not_to raise_error
55
+ end
56
+
57
+ it "succeeds if journaled columns exist when forced" do
58
+ expect { journaled_class.delete_all(force: true) }.not_to raise_error
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "an instance" do
64
+ let(:job) do
65
+ module TestJob
66
+ def perform
67
+ "foo"
68
+ end
69
+
70
+ module_function :perform
71
+ end
72
+ end
73
+
74
+ subject { journaled_class.enqueue(job) }
75
+
76
+ describe "#update_columns" do
77
+ it "refuses on journaled columns" do
78
+ expect { subject.update_columns(locked_at: nil) }.to raise_error(/aborted by Journaled/)
79
+ end
80
+
81
+ it "succeeds on unjournaled columns" do
82
+ expect { subject.update_columns(handler: "") }.not_to raise_error
83
+ end
84
+
85
+ it "succeeds when forced on journaled columns" do
86
+ expect { subject.update_columns({ locked_at: nil }, force: true) }.not_to raise_error
87
+ end
88
+ end
89
+
90
+ describe "#delete" do
91
+ it "refuses if journaled columns exist" do
92
+ expect { subject.delete }.to raise_error(/aborted by Journaled/)
93
+ end
94
+
95
+ it "succeeds if no journaled columns exist" do
96
+ instance = journaled_class_with_no_journaled_columns.enqueue(job)
97
+ expect { instance.delete }.not_to raise_error
98
+ end
99
+
100
+ it "succeeds if journaled columns exist when forced" do
101
+ expect { subject.delete(force: true) }.not_to raise_error
102
+ end
103
+ end
104
+ end
105
+ end
106
+ # rubocop:enable Rails/SkipsModelValidations