yolk-memento 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +32 -0
- data/TODO +3 -0
- data/VERSION.yml +4 -0
- data/generators/memento_migration/memento_migration_generator.rb +10 -0
- data/generators/memento_migration/templates/migration.rb +23 -0
- data/lib/memento.rb +56 -0
- data/lib/memento/action.rb +32 -0
- data/lib/memento/action/create.rb +41 -0
- data/lib/memento/action/destroy.rb +19 -0
- data/lib/memento/action/update.rb +55 -0
- data/lib/memento/action_controller_methods.rb +19 -0
- data/lib/memento/active_record_methods.rb +74 -0
- data/lib/memento/result.rb +36 -0
- data/lib/memento/session.rb +29 -0
- data/lib/memento/state.rb +71 -0
- data/memento.gemspec +75 -0
- data/rails/init.rb +1 -0
- data/spec/memento/action/create_spec.rb +96 -0
- data/spec/memento/action/destroy_spec.rb +43 -0
- data/spec/memento/action/update_spec.rb +209 -0
- data/spec/memento/action_controller_methods_spec.rb +55 -0
- data/spec/memento/active_record_methods_spec.rb +65 -0
- data/spec/memento/result_spec.rb +89 -0
- data/spec/memento/session_spec.rb +181 -0
- data/spec/memento/state_spec.rb +105 -0
- data/spec/memento_spec.rb +156 -0
- data/spec/spec_helper.rb +91 -0
- metadata +92 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
class FooController < ActionController::Base
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Memento::ActionControllerMethods do
|
10
|
+
|
11
|
+
before do
|
12
|
+
setup_db
|
13
|
+
setup_data
|
14
|
+
@controller = FooController.new
|
15
|
+
@controller.stub!(:current_user).and_return(@user)
|
16
|
+
@headers = {}
|
17
|
+
@controller.stub!(:response).and_return(mock("response", :headers => @headers))
|
18
|
+
end
|
19
|
+
|
20
|
+
after do
|
21
|
+
shutdown_db
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should add memento-method to ActionController::Base" do
|
25
|
+
FooController.private_instance_methods.should include("memento")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should call memento#memento with user and block" do
|
29
|
+
project = Project.create!
|
30
|
+
@controller.send(:memento) do
|
31
|
+
project.update_attribute(:name, "P7")
|
32
|
+
end
|
33
|
+
project.reload.name.should eql("P7")
|
34
|
+
project.memento_states.count.should eql(1)
|
35
|
+
Memento::Session.count.should eql(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should set header X-MementoSessionId" do
|
39
|
+
@controller.send(:memento) { Project.create!.update_attribute(:name, "P7") }
|
40
|
+
@headers.should == {'X-Memento-Session-Id' => Memento::Session.last.id.to_s }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should return result of given block" do
|
44
|
+
@controller.send(:memento) do
|
45
|
+
1 + 2
|
46
|
+
end.should eql(3)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not set header when no session stored" do
|
50
|
+
@controller.send(:memento) { Customer.create! } # not stored
|
51
|
+
@headers['X-MementoSessionId'].should be_nil
|
52
|
+
@headers.should_not have_key('X-Memento-Session-Id')
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Memento::ActiveRecordMethods do
|
4
|
+
|
5
|
+
before do
|
6
|
+
setup_db
|
7
|
+
setup_data
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should declare private methods on Project" do
|
11
|
+
Project.private_instance_methods.should include("record_destroy", "record_update", "record_create")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set hook on create to call Memento" do
|
15
|
+
project = Project.new(:name => "Project X")
|
16
|
+
Memento.instance.should_receive(:add_state).once().with("create", project)
|
17
|
+
project.save!
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should set hook on update to call Memento" do
|
21
|
+
project = Project.create!(:name => "Project X")
|
22
|
+
Memento.instance.should_receive(:add_state).once().with("update", project)
|
23
|
+
project.update_attribute(:name, "Project XY")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should set hook on destroy to call Memento" do
|
27
|
+
project = Project.create!(:name => "Project X")
|
28
|
+
Memento.instance.should_receive(:add_state).once().with("destroy", project)
|
29
|
+
project.destroy
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should define attributes_for_memento and ignore attributes given by options" do
|
33
|
+
Project.create!(:name => "Project X").attributes_for_memento.should == {
|
34
|
+
"id"=>1, "name"=>"Project X", "notes"=>nil, "customer_id"=>nil, "closed_at"=>nil
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should define changes_for_memento and ignore attributes given by options" do
|
39
|
+
project = Project.create!(:name => "Project X")
|
40
|
+
project.name = "A Project"
|
41
|
+
project.updated_at = 5.minutes.ago
|
42
|
+
project.notes = "new"
|
43
|
+
project.ignore_this = 2
|
44
|
+
project.changes_for_memento.should_not == project.changes
|
45
|
+
project.changes_for_memento.should == {"name"=>["Project X", "A Project"], "notes"=>[nil, "new"]}
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should define has_many association to memento_states" do
|
49
|
+
project = Project.create!(:name => "Project X")
|
50
|
+
project.memento_states.should be_empty
|
51
|
+
Memento.instance.memento(@user) { project.update_attribute(:name, "Project Y") }
|
52
|
+
project.memento_states.count.should eql(1)
|
53
|
+
Memento.instance.memento(@user) { project.update_attribute(:name, "Project Y") }
|
54
|
+
project.memento_states.count.should eql(1)
|
55
|
+
Memento.instance.memento(@user) { Project.create!.update_attribute(:name, "Project X") }
|
56
|
+
project.memento_states.count.should eql(1)
|
57
|
+
Project.last.memento_states.count.should eql(2)
|
58
|
+
Memento::State.count.should eql(3)
|
59
|
+
end
|
60
|
+
|
61
|
+
after do
|
62
|
+
shutdown_db
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Memento::Result do
|
4
|
+
|
5
|
+
describe "when initalized with valid object" do
|
6
|
+
before do
|
7
|
+
@object = mock("object", :errors => {})
|
8
|
+
@state = mock("state1")
|
9
|
+
@result = Memento::Result.new(@object, @state)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have an object attribute" do
|
13
|
+
@result.object.should eql(@object)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have an state attribute" do
|
17
|
+
@result.state.should eql(@state)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have an error attribute" do
|
21
|
+
@result.error.should be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be valid" do
|
25
|
+
@result.should be_success
|
26
|
+
@result.should_not be_failed
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "when initalized with object with errors" do
|
31
|
+
before do
|
32
|
+
@object = mock("object", :errors => {:memento_undo => "123"})
|
33
|
+
@result = Memento::Result.new(@object, mock("state1"))
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have an object attribute" do
|
37
|
+
@result.object.should eql(@object)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return error" do
|
41
|
+
@result.error.should eql("123")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be invalid" do
|
45
|
+
@result.should be_failed
|
46
|
+
@result.should_not be_success
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe Memento::ResultArray do
|
53
|
+
|
54
|
+
before do
|
55
|
+
@results = Memento::ResultArray.new()
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should have an empty errors array" do
|
59
|
+
@results.errors.should eql([])
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should have no errors" do
|
63
|
+
@results.should be_success
|
64
|
+
@results.should_not be_failed
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "when Memento::Result without errors added" do
|
68
|
+
before do
|
69
|
+
@object = mock("object", :errors => {:memento_undo => "123"})
|
70
|
+
@results << Memento::Result.new(mock("object2", :errors => {}), mock("state1"))
|
71
|
+
@results << (@with_error = Memento::Result.new(@object, mock("state2")))
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should have two entrys" do
|
75
|
+
@results.size.should eql(2)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should have one error" do
|
79
|
+
@results.errors.size.should eql(1)
|
80
|
+
@results.errors.should eql([@with_error])
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should have an error" do
|
84
|
+
@results.should_not be_success
|
85
|
+
@results.should be_failed
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Memento::Session do
|
4
|
+
|
5
|
+
before do
|
6
|
+
setup_db
|
7
|
+
setup_data
|
8
|
+
@session = Memento::Session.create(:user => @user)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should belong to user" do
|
12
|
+
@session.user.should eql(@user)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should require user" do
|
16
|
+
Memento::Session.create.errors[:user].should eql("can't be blank")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should have_many states" do
|
20
|
+
@session.states.should eql([])
|
21
|
+
@session.states.create!(:action_type => "destroy", :record => Project.create!)
|
22
|
+
@session.states.count.should eql(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "on undo" do
|
26
|
+
before do
|
27
|
+
@states = [@t1 = mock("t1"), @t2 = mock("t2")]
|
28
|
+
@session.stub!(:states).and_return(@states)
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "and all states fail" do
|
32
|
+
before do
|
33
|
+
@t1.stub!(:undo).once().and_return(mock("r1", :success? => false))
|
34
|
+
@t2.stub!(:undo).once().and_return(mock("r2", :success? => false))
|
35
|
+
@states.stub!(:count).and_return(2)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should call undo on all states when undo is called" do
|
39
|
+
@t1.should_receive(:undo).once().and_return(mock("r1", :success? => false))
|
40
|
+
@t2.should_receive(:undo).once().and_return(mock("r2", :success? => false))
|
41
|
+
@session.undo
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should kepp itself" do
|
45
|
+
@session.undo
|
46
|
+
@session.reload
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "and all states succeed" do
|
51
|
+
before do
|
52
|
+
@t1.stub!(:undo).once().and_return(mock("r1", :success? => true, :state => @t1))
|
53
|
+
@t2.stub!(:undo).once().and_return(mock("r2", :success? => true, :state => @t2))
|
54
|
+
@t1.stub!(:destroy).once()
|
55
|
+
@t2.stub!(:destroy).once()
|
56
|
+
@states.stub!(:count).and_return(0)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should destroy itself" do
|
60
|
+
@session.undo
|
61
|
+
Memento::Session.find_by_id(@session.id).should be_nil
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should destroy all states" do
|
65
|
+
@t1.should_receive(:destroy).once()
|
66
|
+
@t2.should_receive(:destroy).once()
|
67
|
+
@session.undo
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "and some states succeed, some fail" do
|
72
|
+
before do
|
73
|
+
@t1.stub!(:undo).once().and_return(mock("r1", :success? => true, :state => @t1))
|
74
|
+
@t2.stub!(:undo).once().and_return(mock("r2", :success? => false, :state => @t2))
|
75
|
+
@t1.stub!(:destroy).once()
|
76
|
+
@states.stub!(:count).and_return(1)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should kepp itself" do
|
80
|
+
@session.undo
|
81
|
+
@session.reload
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should destroy only successful states" do
|
85
|
+
@t1.should_receive(:destroy).once()
|
86
|
+
@t2.should_receive(:destroy).never()
|
87
|
+
@session.undo
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "on undo!" do
|
93
|
+
before do
|
94
|
+
@state1 = @session.states.create!(:action_type => "update", :record => @p1 = Project.create!)
|
95
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => Project.create!)
|
96
|
+
@state2 = @session.states.create!(:action_type => "update", :record => @p2 = Project.create!)
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "and all states succeed" do
|
100
|
+
it "should return ResultsArray" do
|
101
|
+
@session.undo!.should be_a(Memento::ResultArray)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should remove all states" do
|
105
|
+
@session.undo!
|
106
|
+
Memento::State.count.should eql(1)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should remove itself" do
|
110
|
+
@session.undo!
|
111
|
+
Memento::Session.find_by_id(@session.id).should be_nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "and all states fail" do
|
116
|
+
before do
|
117
|
+
@state1.update_attribute(:record_data, {:name => ["A", "B"]})
|
118
|
+
@p1.update_attribute(:name, "C")
|
119
|
+
@state2.update_attribute(:record_data, {:name => ["A", "B"]})
|
120
|
+
@p2.update_attribute(:name, "C")
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should keep all states" do
|
124
|
+
@session.undo! rescue
|
125
|
+
Memento::State.count.should eql(3)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should keep itself" do
|
129
|
+
@session.undo! rescue
|
130
|
+
@session.reload
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise Memento::ErrorOnRewind" do
|
134
|
+
lambda{ @session.undo! }.should raise_error(Memento::ErrorOnRewind)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "and some states succeed, some fail" do
|
139
|
+
before do
|
140
|
+
@state1.update_attribute(:record_data, {:name => ["A", "B"]})
|
141
|
+
@p1.update_attribute(:name, "C")
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should keep all states" do
|
145
|
+
@session.undo! rescue nil
|
146
|
+
Memento::State.count.should eql(3)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should keep itself" do
|
150
|
+
@session.undo! rescue nil
|
151
|
+
@session.reload
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should raise Memento::ErrorOnRewind" do
|
155
|
+
lambda{ @session.undo! }.should raise_error(Memento::ErrorOnRewind)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "with states" do
|
161
|
+
before do
|
162
|
+
@session.states.create!(:action_type => "destroy", :record => Project.create!)
|
163
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => Project.create!)
|
164
|
+
@state2 = @session.states.create!(:action_type => "update", :record => Project.create!)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should destroy all states when destroyed" do
|
168
|
+
Memento::State.count.should eql(3)
|
169
|
+
@session.destroy
|
170
|
+
Memento::State.count.should eql(1)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
|
177
|
+
after do
|
178
|
+
shutdown_db
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Memento::State do
|
4
|
+
|
5
|
+
before do
|
6
|
+
setup_db
|
7
|
+
setup_data
|
8
|
+
@session = Memento::Session.create(:user => @user)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should belong to session" do
|
12
|
+
Memento::State.new(:session => @session).session.should eql(@session)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should require session" do
|
16
|
+
Memento::State.create.errors[:session].should eql("can't be blank")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should require action_type to be one of Memento::State::RECORD_CAUSES" do
|
20
|
+
Memento::State.create.errors[:action_type].should eql("can't be blank")
|
21
|
+
Memento::State.create(:action_type => "move").errors[:action_type].should eql("is not included in the list")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should belong to polymorphic record" do
|
25
|
+
Memento::State.new(:record => @user).record.should eql(@user)
|
26
|
+
Memento::State.new(:record => @session).record.should eql(@session)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should require record" do
|
30
|
+
Memento::State.create.errors[:record].should eql("can't be blank")
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
describe "valid State" do
|
35
|
+
before do
|
36
|
+
@state = @session.states.create!(:action_type => "destroy", :record => @project = Project.create(:name => "A") )
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should give back Memento::Result on undo" do
|
40
|
+
result = @state.undo
|
41
|
+
result.should be_a(Memento::Result)
|
42
|
+
result.object.should be_a(Project)
|
43
|
+
result.state.should eql(@state)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should give back old data on record_data" do
|
47
|
+
@state.record_data.should == (@project.attributes_for_memento)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should give back new unsaved copy of object on new_object" do
|
51
|
+
@state.new_object.should be_kind_of(Project)
|
52
|
+
@state.new_object.name.should be_nil
|
53
|
+
@state.new_object do |object|
|
54
|
+
object.name = "B"
|
55
|
+
end.name.should eql("B")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should give back new unsaved copy filled with old data of object on rebuild_object" do
|
59
|
+
@state.rebuild_object.should be_kind_of(Project)
|
60
|
+
@state.rebuild_object.name.should eql("A")
|
61
|
+
@state.rebuild_object(:id).id.should be_nil # skip id
|
62
|
+
@state.rebuild_object.id.should eql(@project.id)
|
63
|
+
@state.rebuild_object do |object|
|
64
|
+
object.name = "B"
|
65
|
+
end.name.should eql("B")
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "on later_states_on_record_for" do
|
69
|
+
|
70
|
+
it "should return empty array" do
|
71
|
+
@state.later_states_on_record_for("destroy").should eql([])
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should return filled array when other record of the given action_type exists" do
|
75
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => @project )
|
76
|
+
@state.later_states_on_record_for("destroy").map(&:class).should eql([Memento::State])
|
77
|
+
@state.later_states_on_record_for(:"destroy").map(&:id).should eql([2])
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should return empty array when only records of another action_type exists" do
|
81
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "update", :record => @project )
|
82
|
+
@state.later_states_on_record_for("destroy").should eql([])
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should return empty array when only destroy records of another record exists" do
|
86
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => Project.create(:name => "B") )
|
87
|
+
@state.later_states_on_record_for("destroy").should eql([])
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should return empty array when only destroy records older than @tack exist" do
|
91
|
+
state2 = Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => @project )
|
92
|
+
state2.update_attribute(:created_at, 3.minutes.ago)
|
93
|
+
@state.later_states_on_record_for("destroy").should eql([])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
after do
|
102
|
+
shutdown_db
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|