authenticator_server 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4ee8fb9d34d02688c3ac99f5f98d36a534a75c90
4
+ data.tar.gz: 6ff12d221468a10314298c95528386f8ad8cc9ea
5
+ SHA512:
6
+ metadata.gz: 3b27df060f23c851f98d9e336a05d11541909196da2745c133cba10a3da3d3d64e0bd3d2ea8525e27023367763c62c54f580b5f172a1615b7258de2e4224c5af
7
+ data.tar.gz: acc68cfa1e033558d12ac052ff4547e4182af8226cca8e28170de7b5a1cc2848242976b0e93ee6a9778df021c1c6b1d1cf83f3c6338f19eba7b68296e6f299d5
@@ -0,0 +1,6 @@
1
+ require "authenticator_server/version"
2
+ require "authenticator_server/token_verifier"
3
+ require "authenticator_server/rsa_key_pair_provider"
4
+
5
+ module AuthenticatorServer
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ require 'openssl'
3
+
4
+ # AuthenticatorServer::RSAKeyPairProvider class is to generate RSA public/private key pair.
5
+ # It uses openssl for key pair generation.
6
+ # @author Shobhit Dixit
7
+ module AuthenticatorServer
8
+ class RSAKeyPairProvider
9
+ KEY_LENGTH = 2048
10
+
11
+ @rsa_public_key = nil
12
+ @rsa_private_key = nil
13
+
14
+ # Constructor to initialize class fields
15
+ #
16
+ # @author Shobhit Dixit
17
+ def initialize
18
+ @rsa_private_key = OpenSSL::PKey::RSA.generate KEY_LENGTH
19
+ @rsa_public_key = @rsa_private_key.public_key
20
+ end
21
+
22
+ # Method to return OpenSSL::PKey::RSA public_key in base64 string
23
+ #
24
+ # @return [String] public_key in base64 string format
25
+ # @author Shobhit Dixit
26
+ def public_key_base64
27
+ @rsa_public_key.to_s
28
+ end
29
+
30
+ # Method to return OpenSSL::PKey::RSA private_key in base64 string
31
+ #
32
+ # @return [String] private_key in base64 string format
33
+ # @author Shobhit Dixit
34
+ def private_key_base64
35
+ @rsa_private_key.to_s
36
+ end
37
+
38
+ # Method to convert public key base64 string to OpenSSL::PKey::RSA object
39
+ #
40
+ # @param public_key_base64 [String] string which will be converted into OpenSSL::PKey::RSA object
41
+ # @return [Object] OpenSSL::PKey::RSA object
42
+ # @author Shobhit Dixit
43
+ def self.public_key_from_base64(public_key_base64)
44
+ raise ArgumentError.new('String argument is expected') unless public_key_base64.kind_of? String
45
+ public_key = OpenSSL::PKey::RSA.new public_key_base64
46
+ raise ArgumentError.new('Not a valid public key') if public_key.private?
47
+ public_key
48
+ end
49
+
50
+ # Method to convert private key base64 string to OpenSSL::PKey::RSA object
51
+ #
52
+ # @param private_key_base64 [String] string which will be converted into OpenSSL::PKey::RSA object
53
+ # @return [Object] OpenSSL::PKey::RSA object
54
+ # @author Shobhit Dixit
55
+ def self.private_key_from_base64(private_key_base64)
56
+ raise ArgumentError.new('String argument is expected') unless private_key_base64.kind_of? String
57
+ private_key = OpenSSL::PKey::RSA.new private_key_base64
58
+ raise ArgumentError.new('Not a valid private key') unless private_key.private?
59
+ private_key
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+ require 'jwt'
3
+
4
+ # AuthenticatorServer::TokenVerifier is used to verify the given token using the given
5
+ # token_verification_key. This is an abstract class that uses jwt under the hood.
6
+ # It is mainly created so as to include this gem in every ROR microservice and use
7
+ # it for token verification or authentication.
8
+ # @author Shobhit Dixit
9
+ module AuthenticatorServer
10
+ class TokenVerifier
11
+
12
+ # ALGORITHM = 'RS256'
13
+ ALGORITHM = 'HS512'
14
+
15
+ # This method verifies the token using token_verification_key
16
+ #
17
+ # @param token [String] jwt to be verified
18
+ # @param token_verification_key [String] secret key use to verify the token
19
+ # @return [Object] on success return the payload
20
+ # @author Shobhit Dixit
21
+ def self.verify(token, token_verification_key)
22
+ begin
23
+ raise ArgumentError.new('String argument is expected') unless token_verification_key.kind_of? String
24
+ # rsa_public_key = AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64 token_verification_key
25
+ output = JWT.decode(token, token_verification_key, true, { algorithm: ALGORITHM })
26
+ output.first
27
+ rescue ArgumentError => e
28
+ argument_error_message e
29
+ rescue JWT::IncorrectAlgorithm => e
30
+ incorrect_algorithm_message e
31
+ rescue JWT::ExpiredSignature => e
32
+ expired_signature_message e
33
+ rescue JWT::VerificationError => e
34
+ verification_error_message e
35
+ rescue JWT::DecodeError => e
36
+ decode_error_message e
37
+ rescue Exception => e
38
+ server_exception_message e
39
+ end
40
+ end
41
+
42
+ class << self
43
+
44
+ private
45
+
46
+ def argument_error_message(exception)
47
+ {
48
+ 'error' => exception.message,
49
+ 'reason' => "IncorrectArgument"
50
+ }
51
+ end
52
+
53
+ def incorrect_algorithm_message(exception)
54
+ {
55
+ 'error' => 'Expected a different algorithm',
56
+ 'reason' => "IncorrectAlgorithm"
57
+ }
58
+ end
59
+
60
+ def expired_signature_message(exception)
61
+ {
62
+ 'error' => 'Token is expired',
63
+ 'reason' => "ExpiredToken"
64
+ }
65
+ end
66
+
67
+ def verification_error_message(exception)
68
+ {
69
+ 'error' => 'Invalid signature',
70
+ 'reason' => "InvalidSignature"
71
+ }
72
+ end
73
+
74
+ def decode_error_message(exception)
75
+ {
76
+ 'error' => exception.message,
77
+ 'reason' => "DecodeError"
78
+ }
79
+ end
80
+
81
+ def server_exception_message(exception)
82
+ {
83
+ 'error' => exception.message,
84
+ 'reason' => "ServerException"
85
+ }
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module AuthenticatorServer
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe AuthenticatorServer::RSAKeyPairProvider do
5
+ let(:provider) { AuthenticatorServer::RSAKeyPairProvider.new }
6
+
7
+ describe "constants and fields" do
8
+
9
+ it "should have a constant KEY_LENGTH" do
10
+ expect(AuthenticatorServer::RSAKeyPairProvider::KEY_LENGTH).to equal 2048
11
+ end
12
+
13
+ it "should have exactly two instance variables rsa_private_key and rsa_public_key" do
14
+ expect(AuthenticatorServer::RSAKeyPairProvider.instance_variables).to contain_exactly(:@rsa_public_key, :@rsa_private_key)
15
+ end
16
+ end
17
+
18
+ describe ".initialize" do
19
+
20
+ it "should initialize instance variables rsa_private_key and rsa_public_key" do
21
+ expect(provider.instance_variable_get(:@rsa_public_key)).not_to be_nil
22
+ expect(provider.instance_variable_get(:@rsa_private_key)).not_to be_nil
23
+ end
24
+
25
+ it "should initialize instance variables rsa_private_key and rsa_public_key with OpenSSL::PKey::RSA object" do
26
+ expect(provider.instance_variable_get(:@rsa_public_key)).to be_a_kind_of(OpenSSL::PKey::RSA)
27
+ expect(provider.instance_variable_get(:@rsa_private_key)).to be_a_kind_of(OpenSSL::PKey::RSA)
28
+ end
29
+
30
+ it "should derive @rsa_public_key from @rsa_private_key" do
31
+ expect(provider.instance_variable_get(:@rsa_public_key).to_s).to be_eql((provider.instance_variable_get(:@rsa_private_key)).public_key.to_s)
32
+ end
33
+ end
34
+
35
+ describe "#public_key_base64" do
36
+
37
+ it "should be present" do
38
+ expect(provider).to respond_to(:public_key_base64)
39
+ end
40
+
41
+ it "should not return nil or empty string" do
42
+ expect(provider.public_key_base64).not_to be_nil
43
+ expect(provider.public_key_base64.strip).not_to be_empty
44
+ end
45
+
46
+ it "should call to_s method on @rsa_public_key" do
47
+ expect(provider.instance_variable_get(:@rsa_public_key)).to receive(:to_s)
48
+ provider.public_key_base64
49
+ end
50
+ end
51
+
52
+ describe "#private_key_base64" do
53
+
54
+ it "should be present" do
55
+ expect(provider).to respond_to(:private_key_base64)
56
+ end
57
+
58
+ it "should not return nil or empty string" do
59
+ expect(provider.private_key_base64).not_to be_nil
60
+ expect(provider.private_key_base64.strip).not_to be_empty
61
+ end
62
+
63
+ it "should call to_s method on @rsa_private_key" do
64
+ expect(provider.instance_variable_get(:@rsa_private_key)).to receive(:to_s)
65
+ provider.private_key_base64
66
+ end
67
+ end
68
+
69
+ describe ".public_key_from_base64" do
70
+ let(:private_key) { OpenSSL::PKey::RSA.generate 2048 }
71
+
72
+ it "should be present and accept exactly 1 argument" do
73
+ expect(AuthenticatorServer::RSAKeyPairProvider).to respond_to(:public_key_from_base64).with(1).argument
74
+ end
75
+
76
+ context "when argument is not a string" do
77
+ it "should raise ArgumentError" do
78
+ expect{AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64(private_key)}.to raise_error(ArgumentError, "String argument is expected")
79
+ end
80
+ end
81
+
82
+ context "when argument is not a valid RSA public/private base64 key" do
83
+ it "should raise OpenSSL::PKey::RSAError" do
84
+ expect{AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64("")}.to raise_error(OpenSSL::PKey::RSAError)
85
+ expect{AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64("Some random text")}.to raise_error(OpenSSL::PKey::RSAError)
86
+ end
87
+ end
88
+
89
+ context "when argument is a valid RSA private base64 key" do
90
+ it "should raise ArgumentError" do
91
+ expect{ AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64(private_key.to_s)}.to raise_error(ArgumentError, "Not a valid public key")
92
+ end
93
+ end
94
+
95
+ context "when argument is a valid RSA public base64 key" do
96
+ let(:public_key) { private_key.public_key }
97
+
98
+ it "should not return nil" do
99
+ expect( AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64 public_key.to_s ).not_to be_nil
100
+ end
101
+
102
+ it "should return OpenSSL::PKey::RSA object" do
103
+ expect(AuthenticatorServer::RSAKeyPairProvider.public_key_from_base64 public_key.to_s).to be_a_kind_of OpenSSL::PKey::RSA
104
+ end
105
+ end
106
+ end
107
+
108
+ describe ".private_key_from_base64" do
109
+ let(:private_key) { OpenSSL::PKey::RSA.generate 2048 }
110
+
111
+ it "should be present and accept exactly 1 argument" do
112
+ expect(AuthenticatorServer::RSAKeyPairProvider).to respond_to(:private_key_from_base64).with(1).argument
113
+ end
114
+
115
+ context "when argument is not a string" do
116
+ it "should raise ArgumentError" do
117
+ expect{AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64(private_key)}.to raise_error(ArgumentError, "String argument is expected")
118
+ end
119
+ end
120
+
121
+ context "when argument is not a valid RSA public/private base64 key" do
122
+ it "should raise OpenSSL::PKey::RSAError" do
123
+ expect{AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64("")}.to raise_error(OpenSSL::PKey::RSAError)
124
+ expect{AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64("Some random text")}.to raise_error(OpenSSL::PKey::RSAError)
125
+ end
126
+ end
127
+
128
+ context "when argument is a valid RSA public base64 key" do
129
+ let(:public_key) { private_key.public_key }
130
+ it "should raise ArgumentError" do
131
+ expect{ AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64(public_key.to_s)}.to raise_error(ArgumentError, "Not a valid private key")
132
+ end
133
+ end
134
+
135
+ context "when argument is a valid RSA private base64 key" do
136
+ it "should not return nil" do
137
+ expect( AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64 private_key.to_s ).not_to be_nil
138
+ end
139
+
140
+ it "should return OpenSSL::PKey::RSA object" do
141
+ expect(AuthenticatorServer::RSAKeyPairProvider.private_key_from_base64 private_key.to_s).to be_a_kind_of OpenSSL::PKey::RSA
142
+ end
143
+ end
144
+ end
145
+
146
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'securerandom'
4
+
5
+ RSpec.describe AuthenticatorServer::TokenVerifier do
6
+ # let (:rsa_private_key) {OpenSSL::PKey::RSA.generate 2048}
7
+ # let (:public_key_base64) {rsa_private_key.public_key.to_s}
8
+ let (:hmac_secret) { SecureRandom.hex }
9
+ let (:encode_key) { hmac_secret }
10
+ let (:decode_key) { hmac_secret }
11
+ let (:payload) { {"data"=>"test payload"} }
12
+ IMPROPER_HEADER = 0
13
+ IMPROPER_PAYLOAD = 1
14
+ IMPROPER_SIGNATURE = 2
15
+
16
+ # here number_format will represent improper_token number_format
17
+ # if number_format = 0, improper_header
18
+ # if number_format = 1, improper_payload
19
+ # if number_format = 2, improper_signature
20
+ def get_improper_token (number_format)
21
+ splitted_token = get_token(payload).split('.')
22
+ splitted_token[number_format] = splitted_token[number_format][2..-5]
23
+ splitted_token.join('.')
24
+ end
25
+
26
+ def get_token(custom_payload)
27
+ JWT.encode custom_payload, encode_key, AuthenticatorServer::TokenVerifier::ALGORITHM
28
+ end
29
+
30
+ def get_expired_token
31
+ claimed_payload = payload.merge({'exp' => Time.now.to_i - 100})
32
+ get_token(claimed_payload)
33
+ end
34
+
35
+ describe ".verify" do
36
+ it "should be present" do
37
+ expect(AuthenticatorServer::TokenVerifier).to respond_to(:verify)
38
+ end
39
+
40
+ context "when arguments are absent" do
41
+ it "should raise ArgumentError" do
42
+ expect { AuthenticatorServer::TokenVerifier.verify}.to raise_error(ArgumentError)
43
+ end
44
+ end
45
+
46
+ context "when arguments are present" do
47
+ it "should accept exactly 2 argument" do
48
+ expect(AuthenticatorServer::TokenVerifier).to respond_to(:verify).with(2).argument
49
+ end
50
+ context "when token is nil/blank/empty string" do
51
+ it "should return {\"error\"=>\"Nil JSON web token\"}" do
52
+ result = AuthenticatorServer::TokenVerifier.verify(nil, decode_key)
53
+ expect(result).to be_eql({"error"=>"Nil JSON web token", "reason"=>"DecodeError"})
54
+ end
55
+ it "should return {\"error\"=>\"Not enough or too many segments\"}" do
56
+ result = AuthenticatorServer::TokenVerifier.verify(' ', decode_key)
57
+ expect(result).to be_eql({"error"=>"Not enough or too many segments", "reason"=>"DecodeError"})
58
+ end
59
+ end
60
+ end
61
+
62
+ context "when token is not in jwt json format" do
63
+ context "when header is not proper" do
64
+ it "should return error hash with proper message" do
65
+ result = AuthenticatorServer::TokenVerifier.verify(get_improper_token(IMPROPER_HEADER), decode_key)
66
+ expect(result['error']).to be_eql('Invalid segment encoding')
67
+ end
68
+ end
69
+ context "when payload is not proper" do
70
+ it "should return error hash with proper message" do
71
+ result = AuthenticatorServer::TokenVerifier.verify(get_improper_token(IMPROPER_PAYLOAD), decode_key)
72
+ expect(result['error']).to be_eql('Invalid segment encoding')
73
+ end
74
+ end
75
+ context "when signature is not proper" do
76
+ it "should return error hash with proper message" do
77
+ result = AuthenticatorServer::TokenVerifier.verify(get_improper_token(IMPROPER_SIGNATURE), decode_key)
78
+ expect(result['error']).to be_eql('Invalid signature')
79
+ end
80
+ end
81
+
82
+ # it "should return {error: 'Expected a different algorithm'}" do
83
+ #
84
+ # end
85
+ end
86
+
87
+ context "when token is expired" do
88
+ it "should return error hash" do
89
+ result = AuthenticatorServer::TokenVerifier.verify(get_expired_token, decode_key)
90
+ expect(result).to have_key('error')
91
+ end
92
+ it "should return {'error' => 'Signature has expired'} " do
93
+ result = AuthenticatorServer::TokenVerifier.verify(get_expired_token, decode_key)
94
+ expect(result['error']).to be_eql('Token is expired')
95
+ end
96
+ end
97
+
98
+ context "when signature/public_key base64 string is not valid" do
99
+ it "should return error hash" do
100
+ result = AuthenticatorServer::TokenVerifier.verify(get_token(payload), {})
101
+ expect(result).to have_key('error')
102
+ end
103
+ it "should return {'error' => 'String argument is expected'}" do
104
+ result = AuthenticatorServer::TokenVerifier.verify(get_token(payload), {})
105
+ expect(result['error']).to be_eql('String argument is expected')
106
+ end
107
+ end
108
+
109
+ context "when token and signature base64 string is valid" do
110
+ it "should return payload hash" do
111
+ result = AuthenticatorServer::TokenVerifier.verify(get_token(payload), decode_key)
112
+ expect(result).to be_eql(payload)
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ # TODO: token generation
119
+ # TODO: make sure, payload hash keys should be strings, or else reserved claims exceptions will occur
120
+ # From Sarah Mei
121
+ # http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
122
+ # my_hash.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}
123
+
124
+ # TODO: handle EncodeError
125
+
126
+
127
+ end
@@ -0,0 +1,5 @@
1
+ RSpec.describe AuthenticatorServer do
2
+ it "has a version number" do
3
+ expect(AuthenticatorServer::VERSION).not_to be nil
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require "bundler/setup"
2
+
3
+ require 'jwt'
4
+ require "authenticator_server"
5
+ require 'authenticator_server/rsa_key_pair_provider'
6
+ require 'authenticator_server/token_verifier'
7
+
8
+
9
+ RSpec.configure do |config|
10
+ # Enable flags like --only-failures and --next-failure
11
+ config.example_status_persistence_file_path = ".rspec_status"
12
+
13
+ # Disable RSpec exposing methods globally on `Module` and `main`
14
+ config.disable_monkey_patching!
15
+
16
+ config.expect_with :rspec do |c|
17
+ c.syntax = :expect
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: authenticator_server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shobhit Dixit
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: jwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
69
+ description: This gem is a wrapper for auth jwt. This is made in order to be consistent
70
+ or uniform while implementing the security in all the microservices.
71
+ email:
72
+ - shobhit.dixit@metacube.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - lib/authenticator_server.rb
78
+ - lib/authenticator_server/rsa_key_pair_provider.rb
79
+ - lib/authenticator_server/token_verifier.rb
80
+ - lib/authenticator_server/version.rb
81
+ - spec/authenticator_server/rsa_key_pair_provider_spec.rb
82
+ - spec/authenticator_server/token_verifier_spec.rb
83
+ - spec/authenticator_server_spec.rb
84
+ - spec/spec_helper.rb
85
+ homepage: http://www.github.com
86
+ licenses: []
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.4.8
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: This gem is to provide authentication for ruby microservices server
108
+ test_files:
109
+ - spec/authenticator_server_spec.rb
110
+ - spec/authenticator_server/rsa_key_pair_provider_spec.rb
111
+ - spec/authenticator_server/token_verifier_spec.rb
112
+ - spec/spec_helper.rb