consul 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of consul might be problematic. Click here for more details.
- data/Gemfile +2 -8
- data/README.md +275 -0
- data/Rakefile +4 -25
- data/consul.gemspec +20 -96
- data/lib/consul.rb +1 -0
- data/lib/consul/active_record.rb +14 -0
- data/lib/consul/controller.rb +29 -0
- data/lib/consul/errors.rb +4 -3
- data/lib/consul/power.rb +2 -0
- data/lib/consul/version.rb +3 -0
- data/spec/app_root/app/controllers/application_controller.rb +2 -4
- data/spec/app_root/app/controllers/dashboards_controller.rb +12 -1
- data/spec/app_root/app/controllers/songs_controller.rb +3 -4
- data/spec/app_root/app/controllers/users_controller.rb +3 -3
- data/spec/app_root/app/models/power.rb +10 -6
- data/spec/app_root/config/routes.rb +1 -1
- data/spec/app_root/db/migrate/001_create_users.rb +3 -1
- data/spec/consul/active_record_spec.rb +37 -0
- data/spec/consul/power_spec.rb +17 -6
- data/spec/controllers/dashboards_controller_spec.rb +17 -0
- data/spec/controllers/songs_controller_spec.rb +4 -4
- data/spec/controllers/users_controller_spec.rb +3 -3
- data/spec/spec_helper.rb +10 -1
- data/spec/support/spec.opts +4 -0
- data/spec/support/spec_candy.rb +179 -0
- metadata +124 -42
- data/README.rdoc +0 -213
- data/VERSION +0 -1
data/lib/consul.rb
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Consul
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def authorize_values_for(property, options = {})
|
7
|
+
method_defined?(:power) or attr_accessor :power
|
8
|
+
assignable_values_for property, options.merge(:through => lambda { ::Power.current })
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Base.send(:extend, Consul::ActiveRecord)
|
data/lib/consul/controller.rb
CHANGED
@@ -8,6 +8,14 @@ module Consul
|
|
8
8
|
|
9
9
|
module ClassMethods
|
10
10
|
|
11
|
+
def current_power_initializer
|
12
|
+
@current_power_initializer || (superclass.respond_to?(:current_power_initializer) && superclass.current_power_initializer)
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_power_initializer=(initializer)
|
16
|
+
@current_power_initializer = initializer
|
17
|
+
end
|
18
|
+
|
11
19
|
private
|
12
20
|
|
13
21
|
def require_power_check(options = {})
|
@@ -18,6 +26,12 @@ module Consul
|
|
18
26
|
skip_before_filter :unchecked_power, options
|
19
27
|
end
|
20
28
|
|
29
|
+
def current_power(&initializer)
|
30
|
+
self.current_power_initializer = initializer
|
31
|
+
around_filter :with_current_power
|
32
|
+
helper_method :current_power
|
33
|
+
end
|
34
|
+
|
21
35
|
def power(*args)
|
22
36
|
|
23
37
|
args_copy = args.dup
|
@@ -72,6 +86,21 @@ module Consul
|
|
72
86
|
raise Consul::UncheckedPower, "This controller does not check against a power"
|
73
87
|
end
|
74
88
|
|
89
|
+
def current_power
|
90
|
+
@current_power_class && @current_power_class.current
|
91
|
+
end
|
92
|
+
|
93
|
+
def with_current_power(&action)
|
94
|
+
power = instance_eval(&self.class.current_power_initializer) or raise Consul::Error, 'current_power initializer returned nil'
|
95
|
+
@current_power_class = power.class
|
96
|
+
@current_power_class.current = power
|
97
|
+
action.call
|
98
|
+
ensure
|
99
|
+
if @current_power_class
|
100
|
+
@current_power_class.current = nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
75
104
|
end
|
76
105
|
|
77
106
|
end
|
data/lib/consul/errors.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Consul
|
2
|
-
class
|
3
|
-
class
|
4
|
-
class
|
2
|
+
class Error < StandardError; end
|
3
|
+
class Powerless < Error; end
|
4
|
+
class UncheckedPower < Error; end
|
5
|
+
class UnmappedAction < Error; end
|
5
6
|
end
|
data/lib/consul/power.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
class DashboardsController < ApplicationController
|
2
2
|
|
3
|
-
power :
|
3
|
+
power :always_true
|
4
4
|
|
5
5
|
def show
|
6
|
+
observe(current_power)
|
7
|
+
end
|
8
|
+
|
9
|
+
def error
|
10
|
+
raise 'error during action'
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def observe(object)
|
16
|
+
# test spy
|
6
17
|
end
|
7
18
|
|
8
19
|
end
|
@@ -1,13 +1,12 @@
|
|
1
1
|
class SongsController < ApplicationController
|
2
2
|
|
3
|
-
# power check is missing
|
4
|
-
|
5
|
-
skip_power_check :only => :index
|
3
|
+
# power check is missing, but we're skipping it for a single action'
|
4
|
+
skip_power_check :only => :show
|
6
5
|
|
7
6
|
def show
|
8
7
|
end
|
9
8
|
|
10
|
-
def
|
9
|
+
def update
|
11
10
|
end
|
12
11
|
|
13
12
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Power
|
2
2
|
include Consul::Power
|
3
3
|
|
4
|
-
def initialize(user)
|
4
|
+
def initialize(user = nil)
|
5
5
|
@user = user
|
6
6
|
end
|
7
7
|
|
@@ -17,16 +17,20 @@ class Power
|
|
17
17
|
Note.scoped(:joins => :client)
|
18
18
|
end
|
19
19
|
|
20
|
-
power :
|
20
|
+
power :always_true do
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
power :always_false do
|
21
25
|
false
|
22
26
|
end
|
23
27
|
|
24
|
-
power :
|
28
|
+
power :always_nil do
|
25
29
|
nil
|
26
30
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
31
|
+
|
32
|
+
def assignable_user_roles
|
33
|
+
%w[guest admin]
|
30
34
|
end
|
31
35
|
|
32
36
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Consul::ActiveRecord do
|
4
|
+
|
5
|
+
describe '.authorize_values_for' do
|
6
|
+
|
7
|
+
it 'should be a shortcut for .assignable_values_for :attribute, :through => lambda { ::Power.current }' do
|
8
|
+
klass = Note.disposable_copy
|
9
|
+
klass.should_receive(:assignable_values_for).with(:attribute, :option => 'option', :through => kind_of(Proc))
|
10
|
+
klass.class_eval do
|
11
|
+
authorize_values_for :attribute, :option => 'option'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should generate a getter and setter for a @power field' do
|
16
|
+
klass = Note.disposable_copy do
|
17
|
+
authorize_values_for :attribute
|
18
|
+
end
|
19
|
+
song = klass.new
|
20
|
+
song.should respond_to(:power)
|
21
|
+
song.should respond_to(:power=)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should obtain assignable values from Power.current' do
|
25
|
+
Power.current = Power.new
|
26
|
+
klass = User.disposable_copy do
|
27
|
+
authorize_values_for :role
|
28
|
+
end
|
29
|
+
user = klass.new
|
30
|
+
user.assignable_roles.should =~ %w[guest admin]
|
31
|
+
user.should allow_value('guest').for(:role)
|
32
|
+
user.should_not allow_value('invalid-value').for(:role)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/spec/consul/power_spec.rb
CHANGED
@@ -72,15 +72,15 @@ describe Consul::Power do
|
|
72
72
|
end
|
73
73
|
|
74
74
|
it 'should return true when the queried power is not a scope, but returns true' do
|
75
|
-
@user.power.
|
75
|
+
@user.power.always_true?.should be_true
|
76
76
|
end
|
77
77
|
|
78
78
|
it 'should return false when the queried power is not a scope, but returns false' do
|
79
|
-
@user.power.
|
79
|
+
@user.power.always_false?.should be_false
|
80
80
|
end
|
81
81
|
|
82
82
|
it 'should return false when the queried power is not a scope, but returns nil' do
|
83
|
-
@user.power.
|
83
|
+
@user.power.always_nil?.should be_false
|
84
84
|
end
|
85
85
|
|
86
86
|
end
|
@@ -100,15 +100,26 @@ describe Consul::Power do
|
|
100
100
|
end
|
101
101
|
|
102
102
|
it 'should not raise Consul::Powerless when the queried power is not a scope, but returns true' do
|
103
|
-
expect { @user.power.
|
103
|
+
expect { @user.power.always_true! }.to_not raise_error
|
104
104
|
end
|
105
105
|
|
106
106
|
it 'should raise Consul::Powerless when the queried power is not a scope, but returns false' do
|
107
|
-
expect { @user.power.
|
107
|
+
expect { @user.power.always_false! }.to raise_error(Consul::Powerless)
|
108
108
|
end
|
109
109
|
|
110
110
|
it 'should raise Consul::Powerless when the queried power is not a scope, but returns nil' do
|
111
|
-
expect { @user.power.
|
111
|
+
expect { @user.power.always_nil! }.to raise_error(Consul::Powerless)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '.current' do
|
117
|
+
|
118
|
+
it 'should provide a class method to set and get the current Power' do
|
119
|
+
Power.current = 'current power'
|
120
|
+
Power.current.should == 'current power'
|
121
|
+
Power.current = nil
|
122
|
+
Power.current.should be_nil
|
112
123
|
end
|
113
124
|
|
114
125
|
end
|
@@ -6,4 +6,21 @@ describe DashboardsController do
|
|
6
6
|
expect { get :show }.to_not raise_error
|
7
7
|
end
|
8
8
|
|
9
|
+
it 'should define a method #current_power that returns the Power' do
|
10
|
+
controller.should_receive(:observe).with(kind_of(Power))
|
11
|
+
get :show
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set the current power before the request, and nilify it after the request" do
|
15
|
+
controller.send(:current_power).should be_nil
|
16
|
+
Power.should_receive_and_execute(:current=).ordered.with(kind_of(Power))
|
17
|
+
Power.should_receive_and_execute(:current=).ordered.with(nil)
|
18
|
+
get :show
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should nilify the current power even if the action raises an error' do
|
22
|
+
expect { post :error }.to raise_error(/error during action/)
|
23
|
+
Power.current.should be_nil
|
24
|
+
end
|
25
|
+
|
9
26
|
end
|
@@ -2,12 +2,12 @@ require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
2
|
|
3
3
|
describe SongsController do
|
4
4
|
|
5
|
-
it
|
6
|
-
expect { get :show, :id => '
|
5
|
+
it 'should allow to skip a required power check' do
|
6
|
+
expect { get :show, :id => '1' }.to_not raise_error
|
7
7
|
end
|
8
8
|
|
9
|
-
it
|
10
|
-
expect {
|
9
|
+
it "should raise an error if an action is not checked against a power" do
|
10
|
+
expect { put :update, :id => '1' }.to raise_error(Consul::UncheckedPower)
|
11
11
|
end
|
12
12
|
|
13
13
|
end
|
@@ -3,11 +3,11 @@ require File.dirname(__FILE__) + '/../spec_helper'
|
|
3
3
|
describe UsersController do
|
4
4
|
|
5
5
|
it "should raise an error if the checked power is not given" do
|
6
|
-
expect { get :
|
6
|
+
expect { get :update, :id => '1' }.to raise_error(Consul::Powerless)
|
7
7
|
end
|
8
8
|
|
9
|
-
it 'should allow to map actions to another power' do
|
10
|
-
expect { get :
|
9
|
+
it 'should allow to map actions to another power using the :map option' do
|
10
|
+
expect { get :show, :id => '1' }.to_not raise_error
|
11
11
|
end
|
12
12
|
|
13
13
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,12 +5,21 @@ ENV['RAILS_ENV'] ||= 'in_memory'
|
|
5
5
|
|
6
6
|
# Load the Rails environment and testing framework
|
7
7
|
require "#{File.dirname(__FILE__)}/app_root/config/environment"
|
8
|
-
require "#{File.dirname(__FILE__)}/../lib/consul"
|
9
8
|
require 'spec/rails'
|
10
9
|
|
10
|
+
# Load dependencies
|
11
|
+
require 'assignable_values'
|
12
|
+
require 'shoulda-matchers'
|
13
|
+
|
14
|
+
# Load the gem itself
|
15
|
+
require "#{File.dirname(__FILE__)}/../lib/consul"
|
16
|
+
|
11
17
|
# Undo changes to RAILS_ENV
|
12
18
|
silence_warnings {RAILS_ENV = ENV['RAILS_ENV']}
|
13
19
|
|
20
|
+
# Requires supporting files with custom matchers and macros, etc in ./support/ and its subdirectories.
|
21
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
|
22
|
+
|
14
23
|
# Run the migrations
|
15
24
|
ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate")
|
16
25
|
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# From https://makandracards.com/makandra/627-the-definitive-spec_candy-rb-rspec-helper
|
2
|
+
Object.class_eval do
|
3
|
+
|
4
|
+
def should_receive_chain(*parts)
|
5
|
+
setup_expectation_chain(parts)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.new_with_stubs(attrs)
|
9
|
+
new.tap do |obj|
|
10
|
+
obj.stub_existing attrs
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def stub_existing(attrs)
|
15
|
+
attrs.each do |method, value|
|
16
|
+
if respond_to?(method, true)
|
17
|
+
stub(method => value)
|
18
|
+
else
|
19
|
+
raise "Attempted to stub non-existing method ##{method} on a #{self.class.name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def should_receive_and_return(methods_and_values)
|
25
|
+
methods_and_values.each do |method, value|
|
26
|
+
should_receive(method).and_return(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def should_receive_all_with(methods_and_values)
|
31
|
+
methods_and_values.each do |method, value|
|
32
|
+
should_receive(method).with(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_not_receive_and_execute(method)
|
37
|
+
should_receive_and_execute(method, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def should_receive_and_execute(method, negate = false)
|
41
|
+
method_base = method.to_s.gsub(/([\?\!\=\[\]]+)$/, '')
|
42
|
+
method_suffix = $1
|
43
|
+
|
44
|
+
method_called = "_#{method_base}_called#{method_suffix}"
|
45
|
+
method_with_spy = "#{method_base}_with_spy#{method_suffix}"
|
46
|
+
method_without_spy = "#{method_base}_without_spy#{method_suffix}"
|
47
|
+
|
48
|
+
prototype = respond_to?(:singleton_class) ? singleton_class : metaclass
|
49
|
+
prototype.class_eval do
|
50
|
+
|
51
|
+
unless method_defined?(method_with_spy)
|
52
|
+
|
53
|
+
define_method method_called do
|
54
|
+
end
|
55
|
+
|
56
|
+
define_method method_with_spy do |*args, &block|
|
57
|
+
send(method_called, *args)
|
58
|
+
send(method_without_spy, *args, &block)
|
59
|
+
end
|
60
|
+
alias_method_chain method, :spy
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
expectation = negate ? :should_not_receive : :should_receive
|
66
|
+
send(expectation, method_called)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def setup_expectation_chain(parts)
|
72
|
+
obj = self
|
73
|
+
for part in parts
|
74
|
+
if part == parts.last
|
75
|
+
obj = add_expectation_chain_link(obj, part)
|
76
|
+
else
|
77
|
+
next_obj = Spec::Mocks::Mock.new('chain link')
|
78
|
+
add_expectation_chain_link(obj, part).at_least(:once).and_return(next_obj)
|
79
|
+
obj = next_obj
|
80
|
+
end
|
81
|
+
end
|
82
|
+
obj
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_expectation_chain_link(obj, part)
|
86
|
+
if part.is_a?(Array)
|
87
|
+
obj.should_receive(part.first).with(*part[1..-1])
|
88
|
+
else
|
89
|
+
obj.should_receive(part)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
Spec::Example::ExampleGroupMethods.class_eval do
|
96
|
+
|
97
|
+
# Improves it_should_behave_like in some ways:
|
98
|
+
# - It scopes the reused examples so #let und #subject does not bleed into the reusing example groups
|
99
|
+
# - It allows to parametrize the reused example group by appending a hash argument.
|
100
|
+
# Every key/value pair in the hash will become a #let variable for the reused example group
|
101
|
+
# - You can call it with a block. It will be available to the reused example group as let(:block)
|
102
|
+
def it_should_act_like(shared_example_group, environment = {}, &block)
|
103
|
+
description = "as #{shared_example_group}"
|
104
|
+
description << " (#{environment.inspect})" if environment.present?
|
105
|
+
describe description do
|
106
|
+
environment.each do |name, value|
|
107
|
+
let(name) { value }
|
108
|
+
end
|
109
|
+
let(:block) { block } if block
|
110
|
+
it_should_behave_like(shared_example_group)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
Spec::Rails::Example::ModelExampleGroup.class_eval do
|
117
|
+
|
118
|
+
def self.it_should_run_callbacks_in_order(*callbacks)
|
119
|
+
callbacks.push(:ordered => true)
|
120
|
+
it_should_run_callbacks(*callbacks)
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.it_should_run_callbacks(*callbacks)
|
124
|
+
options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
|
125
|
+
reason = callbacks.pop if callbacks.last.is_a?(String)
|
126
|
+
hook = description_parts.last.sub(/^#/, '')
|
127
|
+
should = ['should run callbacks', callbacks.inspect, ('in order' if options[:ordered]), reason].compact.join ' '
|
128
|
+
send(:it, should) do
|
129
|
+
callbacks.each do |callback|
|
130
|
+
expectation = subject.should_receive(callback).once
|
131
|
+
expectation.ordered if options[:ordered]
|
132
|
+
end
|
133
|
+
run_state_machine_callbacks_from_prose(hook) || subject.run_callbacks(hook)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def run_state_machine_callbacks_from_prose(prose)
|
140
|
+
if parts = prose.match(/^(\w+) from ([\:\w]+) to ([\:\w]+)$/)
|
141
|
+
name = parts[1].to_sym
|
142
|
+
from = parts[2].sub(/^:/, '').to_sym
|
143
|
+
to = parts[3].sub(/^:/, '').to_sym
|
144
|
+
transition = StateMachine::Transition.new(subject, subject.class.state_machine, name, from, to)
|
145
|
+
transition.run_callbacks
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
ActiveRecord::Base.class_eval do
|
152
|
+
|
153
|
+
# Prevents the databse from being touched, but still runs all validations.
|
154
|
+
def keep_invalid!
|
155
|
+
errors.stub :empty? => false
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
Class.class_eval do
|
161
|
+
|
162
|
+
define_method :stub_any_instance do |stubs|
|
163
|
+
unstubbed_new = method(:new)
|
164
|
+
stub(:new).and_return do |*args|
|
165
|
+
unstubbed_new.call(*args).tap do |obj|
|
166
|
+
obj.stub stubs
|
167
|
+
end
|
168
|
+
end
|
169
|
+
stubs
|
170
|
+
end
|
171
|
+
|
172
|
+
define_method :disposable_copy do |&body|
|
173
|
+
this = self
|
174
|
+
copy = Class.new(self, &body)
|
175
|
+
copy.singleton_class.send(:define_method, :name) { this.name }
|
176
|
+
copy
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|