mongoid_token 1.1.1 → 2.0.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/.autotest +0 -1
- data/README.md +147 -77
- data/lib/mongoid/token/collision_resolver.rb +37 -0
- data/lib/mongoid/token/collisions.rb +31 -0
- data/lib/mongoid/token/finders.rb +19 -0
- data/lib/mongoid/token/generator.rb +72 -0
- data/lib/mongoid/token/options.rb +68 -0
- data/lib/mongoid/token.rb +63 -0
- data/lib/mongoid_token.rb +1 -116
- data/lib/version.rb +1 -1
- data/spec/mongoid/token/collisions_spec.rb +92 -0
- data/spec/mongoid/token/exceptions_spec.rb +4 -0
- data/spec/mongoid/token/finders_spec.rb +41 -0
- data/spec/mongoid/token/generator_spec.rb +49 -0
- data/spec/mongoid/token/options_spec.rb +70 -0
- data/spec/mongoid/token_spec.rb +183 -200
- data/spec/spec_helper.rb +5 -1
- metadata +18 -2
data/lib/mongoid_token.rb
CHANGED
@@ -1,116 +1 @@
|
|
1
|
-
require 'mongoid/token
|
2
|
-
|
3
|
-
module Mongoid
|
4
|
-
module Token
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
|
7
|
-
module ClassMethods
|
8
|
-
def token(*args)
|
9
|
-
options = args.extract_options!
|
10
|
-
options[:length] ||= 4
|
11
|
-
options[:retry] ||= 3
|
12
|
-
options[:contains] ||= :alphanumeric
|
13
|
-
options[:field_name] ||= :token
|
14
|
-
#options[:key] ||= false
|
15
|
-
|
16
|
-
self.field options[:field_name].to_sym, :type => String
|
17
|
-
self.index({ options[:field_name].to_sym => 1 }, { :unique => true })
|
18
|
-
|
19
|
-
#if options[:key]
|
20
|
-
# self.key options[:field_name].to_sym
|
21
|
-
#end
|
22
|
-
|
23
|
-
set_callback(:create, :before) do |document|
|
24
|
-
document.create_token(options[:length], options[:contains])
|
25
|
-
end
|
26
|
-
|
27
|
-
set_callback(:save, :before) do |document|
|
28
|
-
document.create_token_if_nil(options[:length], options[:contains])
|
29
|
-
end
|
30
|
-
|
31
|
-
after_initialize do # set_callback did not work with after_initialize callback
|
32
|
-
self.instance_variable_set :@max_collision_retries, options[:retry]
|
33
|
-
self.instance_variable_set :@token_field_name, options[:field_name]
|
34
|
-
self.instance_variable_set :@token_length, options[:length]
|
35
|
-
self.instance_variable_set :@token_contains, options[:contains]
|
36
|
-
end
|
37
|
-
|
38
|
-
if options[:retry] > 0
|
39
|
-
alias_method_chain :insert, :safety
|
40
|
-
alias_method_chain :upsert, :safety
|
41
|
-
end
|
42
|
-
|
43
|
-
self.class_variable_set :@@token_field_name, options[:field_name]
|
44
|
-
end
|
45
|
-
|
46
|
-
def find_by_token(token)
|
47
|
-
field_name = self.class_variable_get :@@token_field_name
|
48
|
-
self.find_by(field_name.to_sym => token)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def to_param
|
53
|
-
self.send(@token_field_name.to_sym) || super
|
54
|
-
end
|
55
|
-
|
56
|
-
protected
|
57
|
-
|
58
|
-
def resolve_token_collisions
|
59
|
-
retries = @max_collision_retries
|
60
|
-
|
61
|
-
begin
|
62
|
-
yield
|
63
|
-
rescue Moped::Errors::OperationFailure => e
|
64
|
-
# This is horrible, but seems to be the only way to get the details of the exception?
|
65
|
-
continue unless [11000, 11001].include?(e.details['code'])
|
66
|
-
continue unless e.details['err'] =~ /dup key/ &&
|
67
|
-
e.details['err'] =~ /"#{self.send(@token_field_name.to_sym)}"/
|
68
|
-
|
69
|
-
if (retries -= 1) > 0
|
70
|
-
self.create_token(@token_length, @token_contains)
|
71
|
-
retry
|
72
|
-
else
|
73
|
-
Rails.logger.warn "[Mongoid::Token] Warning: Maximum to generation retries (#{@max_collision_retries}) exceeded." if defined?(Rails) && Rails.env == 'development'
|
74
|
-
raise Mongoid::Token::CollisionRetriesExceeded.new(self, @max_collision_retries)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def insert_with_safety(options = {})
|
80
|
-
resolve_token_collisions { with(:safe => true).insert_without_safety(options) }
|
81
|
-
end
|
82
|
-
|
83
|
-
def upsert_with_safety(options = {})
|
84
|
-
resolve_token_collisions { with(:safe => true).upsert_without_safety(options) }
|
85
|
-
end
|
86
|
-
|
87
|
-
def create_token(length, characters)
|
88
|
-
self.send(:"#{@token_field_name}=", self.generate_token(length, characters))
|
89
|
-
end
|
90
|
-
|
91
|
-
def create_token_if_nil(length, characters)
|
92
|
-
if self[@token_field_name.to_sym].blank?
|
93
|
-
self.create_token(length, characters)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def generate_token(length, characters = :alphanumeric)
|
98
|
-
case characters
|
99
|
-
when :alphanumeric
|
100
|
-
(1..length).collect { (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
|
101
|
-
when :numeric
|
102
|
-
rand(10**length).to_s
|
103
|
-
when :fixed_numeric
|
104
|
-
rand(10**length).to_s.rjust(length,rand(10).to_s)
|
105
|
-
when :fixed_numeric_no_leading_zeros
|
106
|
-
(rand(10**length - 10**(length-1)) + 10**(length-1)).to_s
|
107
|
-
when :alpha
|
108
|
-
Array.new(length).map{['A'..'Z','a'..'z'].map{|r|r.to_a}.flatten[rand(52)]}.join
|
109
|
-
when :alpha_lower
|
110
|
-
Array.new(length).map{['a'..'z'].map{|r|r.to_a}.flatten[rand(26)]}.join
|
111
|
-
when :alpha_upper
|
112
|
-
Array.new(length).map{['A'..'Z'].map{|r|r.to_a}.flatten[rand(26)]}.join
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
1
|
+
require 'mongoid/token'
|
data/lib/version.rb
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Collisions do
|
4
|
+
let(:document) { Object.new }
|
5
|
+
describe "#resolve_token_collisions" do
|
6
|
+
context "when there is a duplicate token" do
|
7
|
+
let(:resolver) { double("Mongoid::Token::CollisionResolver") }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
resolver.stub(:field_name).and_return(:token)
|
11
|
+
resolver.stub(:create_new_token_for){|doc|}
|
12
|
+
document.class.send(:include, Mongoid::Token::Collisions)
|
13
|
+
document.stub(:is_duplicate_token_error?).and_return(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
context "and there are zero retries" do
|
17
|
+
it "should raise an error after the first try" do
|
18
|
+
resolver.stub(:retry_count).and_return(0)
|
19
|
+
attempts = 0
|
20
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Token::CollisionRetriesExceeded
|
21
|
+
expect(attempts).to eq 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "and retries is set to 1" do
|
26
|
+
it "should raise an error after retrying once" do
|
27
|
+
resolver.stub(:retry_count).and_return(1)
|
28
|
+
attempts = 0
|
29
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Token::CollisionRetriesExceeded
|
30
|
+
expect(attempts).to eq 2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "and retries is greater than 1" do
|
35
|
+
it "should raise an error after retrying" do
|
36
|
+
resolver.stub(:retry_count).and_return(3)
|
37
|
+
attempts = 0
|
38
|
+
expect{document.resolve_token_collisions(resolver) { attempts += 1; raise Moped::Errors::OperationFailure.new("","") }}.to raise_error Mongoid::Token::CollisionRetriesExceeded
|
39
|
+
expect(attempts).to eq 4
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#raise_collision_retries_exceeded_error" do
|
46
|
+
before(:each) do
|
47
|
+
document.class.send(:include, Mongoid::Token::Collisions)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should warn the rails logger" do
|
51
|
+
message = nil
|
52
|
+
|
53
|
+
stub_const("Rails", Class.new)
|
54
|
+
|
55
|
+
logger = double("logger")
|
56
|
+
logger.stub("warn"){ |msg| message = msg }
|
57
|
+
Rails.stub("logger").and_return(logger)
|
58
|
+
|
59
|
+
begin
|
60
|
+
document.raise_collision_retries_exceeded_error(:token, 3)
|
61
|
+
rescue
|
62
|
+
end
|
63
|
+
expect(message).to_not be_nil
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise an error" do
|
67
|
+
expect{ document.raise_collision_retries_exceeded_error(:token, 3) }.to raise_error(Mongoid::Token::CollisionRetriesExceeded)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#is_duplicate_token_error?" do
|
72
|
+
before(:each) do
|
73
|
+
document.class.send(:include, Mongoid::Token::Collisions)
|
74
|
+
end
|
75
|
+
context "when there is a duplicate key error" do
|
76
|
+
it "should return true" do
|
77
|
+
document.stub("token").and_return("tokenvalue123")
|
78
|
+
err = double("Moped::Errors::OperationFailure")
|
79
|
+
err.stub("details").and_return do
|
80
|
+
{
|
81
|
+
"err" => "E11000 duplicate key error index: mongoid_token_test.links.$token_1 dup key: { : \"tokenvalue123\" }",
|
82
|
+
"code" => 11000,
|
83
|
+
"n" => 0,
|
84
|
+
"connectionId" => 130,
|
85
|
+
"ok" => 1.0
|
86
|
+
}
|
87
|
+
document.is_duplicate_token_error?(err, document, :token)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Finders do
|
4
|
+
before :each do
|
5
|
+
end
|
6
|
+
|
7
|
+
it "define a finder based on a field_name" do
|
8
|
+
klass = Class.new
|
9
|
+
field = :another_token
|
10
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(klass, field)
|
11
|
+
klass.singleton_methods.should include(:"find_by_#{field}")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "override the `find` method of the document" do
|
15
|
+
klass = Class.new
|
16
|
+
klass.define_singleton_method(:find) {|*args| :original_find }
|
17
|
+
klass.define_singleton_method(:find_by) {|*args| :token_find }
|
18
|
+
|
19
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(klass)
|
20
|
+
|
21
|
+
klass.find(Moped::BSON::ObjectId.new).should == :original_find
|
22
|
+
klass.find(Moped::BSON::ObjectId.new, Moped::BSON::ObjectId.new).should == :original_find
|
23
|
+
klass.find().should == :original_find
|
24
|
+
klass.find(Moped::BSON::ObjectId.new, "token").should == :token_find
|
25
|
+
klass.find("token").should == :token_find
|
26
|
+
end
|
27
|
+
|
28
|
+
it "retrieve a document using the dynamic finder" do
|
29
|
+
class Document; include Mongoid::Document; field :token; end
|
30
|
+
document = Document.create!(:token => "1234")
|
31
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(Document)
|
32
|
+
Document.find_by_token("1234").should == document
|
33
|
+
end
|
34
|
+
|
35
|
+
it "retrieve a document using the `find` method" do
|
36
|
+
class Document; include Mongoid::Document; field :token; end
|
37
|
+
document = Document.create! :token => "1234"
|
38
|
+
Mongoid::Token::Finders.define_custom_token_finder_for(Document)
|
39
|
+
Document.find("1234").should == document
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Generator do
|
4
|
+
describe "#generate" do
|
5
|
+
it "generates lowercase characters" do
|
6
|
+
100.times{ Mongoid::Token::Generator.generate("%c").should =~ /[a-z]/ }
|
7
|
+
end
|
8
|
+
|
9
|
+
it "generates uppercase characters" do
|
10
|
+
100.times{ Mongoid::Token::Generator.generate("%C").should =~ /[A-Z]/ }
|
11
|
+
end
|
12
|
+
|
13
|
+
it "generates digits" do
|
14
|
+
100.times{ Mongoid::Token::Generator.generate("%d").should =~ /[0-9]/ }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "generates non-zero digits" do
|
18
|
+
100.times{ Mongoid::Token::Generator.generate("%D").should =~ /[1-9]/ }
|
19
|
+
end
|
20
|
+
|
21
|
+
it "generates alphanumeric characters" do
|
22
|
+
100.times{ Mongoid::Token::Generator.generate("%s").should =~ /[A-Za-z0-9]/ }
|
23
|
+
end
|
24
|
+
|
25
|
+
it "generates upper and lowercase characters" do
|
26
|
+
100.times{ Mongoid::Token::Generator.generate("%w").should =~ /[A-Za-z]/ }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "generates URL-safe punctuation" do
|
30
|
+
100.times{ Mongoid::Token::Generator.generate("%p").should =~ /[\.\-\_\=\+\$]/ }
|
31
|
+
end
|
32
|
+
|
33
|
+
it "generates patterns of a fixed length" do
|
34
|
+
100.times{ Mongoid::Token::Generator.generate("%s8").should =~ /[A-Za-z0-9]{8}/ }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "generates patterns of a variable length" do
|
38
|
+
100.times{ Mongoid::Token::Generator.generate("%s1,5").should =~ /[A-Za-z0-9]{1,5}/ }
|
39
|
+
end
|
40
|
+
|
41
|
+
it "generates patterns with static prefixes/suffixes" do
|
42
|
+
100.times { Mongoid::Token::Generator.generate("prefix-%s4-suffix").should =~ /prefix\-[A-Za-z0-9]{4}\-suffix/ }
|
43
|
+
end
|
44
|
+
|
45
|
+
it "generates more complex patterns" do
|
46
|
+
100.times { Mongoid::Token::Generator.generate("pre-%d4-%C3-%d4").should =~ /pre\-[0-9]{4}\-[A-Z]{3}\-[0-9]{4}/ }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[.. .. spec_helper])
|
2
|
+
|
3
|
+
describe Mongoid::Token::Options do
|
4
|
+
before do
|
5
|
+
@options = Mongoid::Token::Options.new(
|
6
|
+
{
|
7
|
+
:length => 9999,
|
8
|
+
:retry_count => 8888,
|
9
|
+
:contains => :nonsense,
|
10
|
+
:field_name => :not_a_token
|
11
|
+
}
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have a length" do
|
16
|
+
@options.length.should == 9999
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should default to a length of 4" do
|
20
|
+
Mongoid::Token::Options.new.length.should == 4
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have a retry count" do
|
24
|
+
@options.retry_count.should == 8888
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should default to a retry count of 3" do
|
28
|
+
Mongoid::Token::Options.new.retry_count.should == 3
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should have a list of characters to contain" do
|
32
|
+
@options.contains.should == :nonsense
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should default to an alphanumeric set of characters to contain" do
|
36
|
+
Mongoid::Token::Options.new.contains.should == :alphanumeric
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should have a field name" do
|
40
|
+
@options.field_name.should == :not_a_token
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should default to a field name of 'token'" do
|
44
|
+
Mongoid::Token::Options.new.field_name.should == :token
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should create a pattern" do
|
48
|
+
Mongoid::Token::Options.new.pattern.should == "%s4"
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "override_to_param" do
|
52
|
+
it "should be an option" do
|
53
|
+
expect(Mongoid::Token::Options.new({:override_to_param => false}).override_to_param?).to eq false
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should default to true" do
|
57
|
+
expect(Mongoid::Token::Options.new.override_to_param?).to eq true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "skip_finder" do
|
62
|
+
it "should be an option" do
|
63
|
+
expect(Mongoid::Token::Options.new({:skip_finders => true}).skip_finders?).to eq true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should default to false" do
|
67
|
+
expect(Mongoid::Token::Options.new.skip_finders?).to eq false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|