memento 0.3.0
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.
- 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
|