memento 0.3.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 +33 -0
- data/TODO +3 -0
- data/VERSION.yml +5 -0
- data/generators/memento_migration/memento_migration_generator.rb +10 -0
- data/generators/memento_migration/templates/migration.rb +23 -0
- data/lib/memento.rb +59 -0
- data/lib/memento/action.rb +40 -0
- data/lib/memento/action/create.rb +37 -0
- data/lib/memento/action/destroy.rb +30 -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 +47 -0
- data/memento.gemspec +79 -0
- data/rails/init.rb +1 -0
- data/spec/memento/action/create_spec.rb +82 -0
- data/spec/memento/action/destroy_spec.rb +43 -0
- data/spec/memento/action/update_spec.rb +213 -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 +117 -0
- data/spec/memento/state_spec.rb +55 -0
- data/spec/memento_spec.rb +169 -0
- data/spec/spec_helper.rb +91 -0
- metadata +95 -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,117 @@
|
|
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
|
+
context "undo" do
|
26
|
+
before do
|
27
|
+
@state1 = @session.states.create!(:action_type => "update", :record => @p1 = Project.create!)
|
28
|
+
@other = Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => Project.create!)
|
29
|
+
@state2 = @session.states.create!(:action_type => "update", :record => @p2 = Project.create!)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "and all states succeed" do
|
33
|
+
it "should return ResultsArray" do
|
34
|
+
@session.undo.should be_a(Memento::ResultArray)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should remove all states" do
|
38
|
+
@session.undo
|
39
|
+
Memento::State.count.should eql(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should remove itself" do
|
43
|
+
@session.undo
|
44
|
+
Memento::Session.find_by_id(@session.id).should be_nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "and all states fail" do
|
49
|
+
before do
|
50
|
+
@state1.update_attribute(:record_data, {:name => ["A", "B"]})
|
51
|
+
@p1.update_attribute(:name, "C")
|
52
|
+
@state2.update_attribute(:record_data, {:name => ["A", "B"]})
|
53
|
+
@p2.update_attribute(:name, "C")
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should keep all states" do
|
57
|
+
@session.undo
|
58
|
+
Memento::State.count.should eql(3)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should keep itself" do
|
62
|
+
@session.undo
|
63
|
+
@session.reload
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise Memento::ErrorOnRewind on undo!" do
|
67
|
+
lambda{ @session.undo! }.should raise_error(Memento::ErrorOnRewind)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "and some states succeed, some fail" do
|
72
|
+
before do
|
73
|
+
@state1.update_attribute(:record_data, {:name => ["A", "B"]})
|
74
|
+
@p1.update_attribute(:name, "C")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should keep all states when using undo!" do
|
78
|
+
@session.undo! rescue
|
79
|
+
Memento::State.count.should eql(3)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should NOT keep all states when using undo" do
|
83
|
+
@session.undo
|
84
|
+
Memento::State.count.should eql(2)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should keep itself" do
|
88
|
+
@session.undo rescue nil
|
89
|
+
@session.reload
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should raise Memento::ErrorOnRewind on undo!" do
|
93
|
+
lambda{ @session.undo! }.should raise_error(Memento::ErrorOnRewind)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "with states" do
|
99
|
+
before do
|
100
|
+
@session.states.create!(:action_type => "destroy", :record => Project.create!)
|
101
|
+
Memento::Session.create!(:user => @user).states.create!(:action_type => "destroy", :record => Project.create!)
|
102
|
+
@state2 = @session.states.create!(:action_type => "update", :record => Project.create!)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should destroy all states when destroyed" do
|
106
|
+
Memento::State.count.should eql(3)
|
107
|
+
@session.destroy
|
108
|
+
Memento::State.count.should eql(1)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
after do
|
114
|
+
shutdown_db
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,55 @@
|
|
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
|
+
end
|
50
|
+
|
51
|
+
after do
|
52
|
+
shutdown_db
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe Memento do
|
4
|
+
before do
|
5
|
+
setup_db
|
6
|
+
setup_data
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
shutdown_db
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be a singleton" do
|
14
|
+
Memento.instance.should be_kind_of(Singleton)
|
15
|
+
Memento.instance.should eql(Memento.instance)
|
16
|
+
lambda{ Memento.new }.should raise_error(NoMethodError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not be memento by default" do
|
20
|
+
Memento.instance.should_not be_active
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "start" do
|
24
|
+
|
25
|
+
before do
|
26
|
+
Memento.instance.start(@user)
|
27
|
+
@session = Memento.instance.instance_variable_get("@session")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should require user or user_id on start" do
|
31
|
+
lambda{ Memento.instance.start }.should raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should set an unsaved memento_session when starting" do
|
35
|
+
Memento::Session.count.should eql(0)
|
36
|
+
@session.should be_kind_of(Memento::Session)
|
37
|
+
@session.should be_new_record
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should set user on session" do
|
41
|
+
@session.user.should eql(User.first)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should set user when passing in id as integer" do
|
45
|
+
Memento.instance.start(User.create(:name => "MyUser2").id)
|
46
|
+
Memento.instance.instance_variable_get("@session").user.should eql(User.last)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not start memento when user does not exists/is invalid" do
|
50
|
+
Memento.instance.stop
|
51
|
+
Memento.instance.start(123333)
|
52
|
+
Memento.instance.should_not be_active
|
53
|
+
Memento.instance.start("123")
|
54
|
+
Memento.instance.should_not be_active
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be memento" do
|
58
|
+
Memento.instance.should be_active
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "stop" do
|
64
|
+
before do
|
65
|
+
Memento.instance.start(@user)
|
66
|
+
Memento.instance.stop
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should not be memento" do
|
70
|
+
Memento.instance.should_not be_active
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should remove session if no states created" do
|
74
|
+
Memento::Session.count.should eql(0)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "memento block" do
|
79
|
+
|
80
|
+
it "should record inside of block and stop after" do
|
81
|
+
Memento.instance.should_not be_active
|
82
|
+
Memento.instance.memento(@user) do
|
83
|
+
Memento.instance.should be_active
|
84
|
+
end
|
85
|
+
Memento.instance.should_not be_active
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should give back session when states created" do
|
89
|
+
Memento.instance.memento(@user) do
|
90
|
+
Project.create!
|
91
|
+
end.should be_a(Memento::Session)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should give back false when no states created" do
|
95
|
+
Memento.instance.memento(@user) do
|
96
|
+
1 + 1
|
97
|
+
end.should be_false
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should raise error in block and stop session" do
|
101
|
+
lambda {
|
102
|
+
Memento.instance.memento(@user) do
|
103
|
+
raise StandardError
|
104
|
+
end.should be_nil
|
105
|
+
}.should raise_error(StandardError)
|
106
|
+
Memento.instance.should_not be_active
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "when active" do
|
112
|
+
before do
|
113
|
+
@project = Project.create(:name => "P1")
|
114
|
+
Memento.instance.start(@user)
|
115
|
+
end
|
116
|
+
|
117
|
+
after do
|
118
|
+
Memento.instance.stop
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should create memento_state for ar-object with action_type" do
|
122
|
+
Memento::State.count.should eql(0)
|
123
|
+
Memento.instance.add_state :destroy, @project
|
124
|
+
Memento::State.count.should eql(1)
|
125
|
+
Memento::State.first.action_type.should eql("destroy")
|
126
|
+
Memento::State.first.record.should eql(Project.last)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should save session on first added state" do
|
130
|
+
Memento::Session.count.should eql(0)
|
131
|
+
Memento.instance.add_state :destroy, @project
|
132
|
+
Memento::Session.count.should eql(1)
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "when ignoring" do
|
136
|
+
it "should NOT create memento_state for ar-object with action_type" do
|
137
|
+
Memento.instance.ignore do
|
138
|
+
Memento.instance.add_state :destroy, Project.create(:name => "P1")
|
139
|
+
end
|
140
|
+
|
141
|
+
Memento::State.count.should eql(0)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "when not active" do
|
148
|
+
|
149
|
+
it "should NOT create memento_state for ar-object with action_type" do
|
150
|
+
Memento.instance.add_state :destroy, Project.create(:name => "P1")
|
151
|
+
Memento::State.count.should eql(0)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
context "serializer" do
|
157
|
+
|
158
|
+
it "should default to yaml" do
|
159
|
+
Memento.serializer.should eql(YAML)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should be changeable" do
|
163
|
+
Memento.serializer = Marshal
|
164
|
+
Memento.serializer.should eql(Marshal)
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|