robust_client_socket 0.5.2
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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +732 -0
- data/Rakefile +12 -0
- data/lib/robust_client_socket/configuration.rb +67 -0
- data/lib/robust_client_socket/http/client.rb +90 -0
- data/lib/robust_client_socket/http/helpers.rb +68 -0
- data/lib/robust_client_socket/http/httparty_overrides.rb +20 -0
- data/lib/robust_client_socket.rb +29 -0
- data/lib/version.rb +5 -0
- data/robust_client_socket.gemspec +33 -0
- data/spec/lib/payrent_client_socket/http/client_spec.rb +137 -0
- data/spec/lib/payrent_client_socket.rb +31 -0
- data/spec/lib/robust_client_socket/configuration_spec.rb +131 -0
- data/spec/lib/robust_client_socket/http/client_spec.rb +137 -0
- data/spec/lib/robust_client_socket/http/helpers_spec.rb +80 -0
- data/spec/lib/robust_client_socket/http/httparty_overrides_spec.rb +65 -0
- data/spec/spec_helper.rb +99 -0
- metadata +114 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module RobustClientSocket
|
|
2
|
+
module Configuration
|
|
3
|
+
MIN_KEY_SIZE = 2048
|
|
4
|
+
|
|
5
|
+
attr_reader :configuration, :configured
|
|
6
|
+
|
|
7
|
+
def configure
|
|
8
|
+
@configuration ||= ConfigStore.new
|
|
9
|
+
yield(configuration)
|
|
10
|
+
validate_keys_security!
|
|
11
|
+
|
|
12
|
+
@configured = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def correct_configuration?
|
|
16
|
+
return false unless configuration.services.is_a?(Hash)
|
|
17
|
+
return false if configuration.services.empty?
|
|
18
|
+
return false if configuration.client_name.nil?
|
|
19
|
+
|
|
20
|
+
configuration.services.all? do |_, creds|
|
|
21
|
+
creds.key?(:base_uri) && creds.key?(:public_key)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configured?
|
|
26
|
+
!!@configured && correct_configuration?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_keys_security!
|
|
32
|
+
configuration.services.each do |service_name, creds|
|
|
33
|
+
next unless creds[:public_key]
|
|
34
|
+
|
|
35
|
+
key = OpenSSL::PKey::RSA.new(creds[:public_key])
|
|
36
|
+
key_bits = key.n.num_bits
|
|
37
|
+
|
|
38
|
+
if key_bits < MIN_KEY_SIZE
|
|
39
|
+
raise SecurityError,
|
|
40
|
+
"RSA key size for #{service_name} (#{key_bits} bits) below minimum (#{MIN_KEY_SIZE} bits)"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise SecurityError, "Invalid public key for #{service_name}" unless key.public?
|
|
44
|
+
rescue OpenSSL::PKey::RSAError => e
|
|
45
|
+
raise SecurityError, "Invalid public key for #{service_name}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class ConfigStore
|
|
51
|
+
attr_reader :services
|
|
52
|
+
attr_accessor :client_name, :header_name
|
|
53
|
+
|
|
54
|
+
def initialize
|
|
55
|
+
@services = {}
|
|
56
|
+
@client_name = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def method_missing(name, *args) # rubocop:disable Style/MissingRespondToMissing
|
|
60
|
+
if name.end_with?('=')
|
|
61
|
+
@services[name.to_s.delete_suffix('=').to_sym] = args.first.is_a?(Hash) && args.pop
|
|
62
|
+
else
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module RobustClientSocket
|
|
2
|
+
module HTTP
|
|
3
|
+
class Client
|
|
4
|
+
include Helpers
|
|
5
|
+
include HTTParty
|
|
6
|
+
include HTTPartyOverrides
|
|
7
|
+
|
|
8
|
+
singleton_class.attr_accessor :credentials, :client_name, :header_name
|
|
9
|
+
|
|
10
|
+
InsecureConnectionError = Class.new(StandardError)
|
|
11
|
+
InvalidCredentialsError = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def init(credentials:, client_name:, header_name: nil)
|
|
15
|
+
validate_credentials!(credentials)
|
|
16
|
+
|
|
17
|
+
if credentials.fetch(:ssl_verify, false)
|
|
18
|
+
configure_ssl!(credentials)
|
|
19
|
+
enforce_https!(credentials)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
self.credentials = credentials
|
|
23
|
+
self.client_name = client_name
|
|
24
|
+
self.header_name = header_name
|
|
25
|
+
|
|
26
|
+
base_uri credentials[:base_uri]
|
|
27
|
+
headers robust_headers
|
|
28
|
+
configure_timeouts(credentials)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_credentials!(credentials)
|
|
34
|
+
required_keys = [:base_uri, :public_key]
|
|
35
|
+
missing = required_keys - credentials.keys
|
|
36
|
+
|
|
37
|
+
raise InvalidCredentialsError, "Missing keys: #{missing.join(', ')}" if missing.any?
|
|
38
|
+
raise InvalidCredentialsError, "base_uri cannot be empty" if credentials[:base_uri].to_s.strip.empty?
|
|
39
|
+
raise InvalidCredentialsError, "public_key cannot be empty" if credentials[:public_key].to_s.strip.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def enforce_https!(credentials)
|
|
43
|
+
return if credentials[:base_uri].start_with?('https://')
|
|
44
|
+
return unless production?
|
|
45
|
+
|
|
46
|
+
raise InsecureConnectionError,
|
|
47
|
+
"HTTPS required in production. Use https:// instead of #{credentials[:base_uri]}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def configure_ssl!(credentials)
|
|
51
|
+
default_options.update(
|
|
52
|
+
verify: true,
|
|
53
|
+
ssl_version: :TLSv1_2,
|
|
54
|
+
ciphers: ssl_ciphers(credentials),
|
|
55
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ssl_ciphers(credentials)
|
|
60
|
+
ciphers = credentials.fetch(:ciphers,
|
|
61
|
+
%w[ECDHE-RSA-AES128-GCM-SHA256
|
|
62
|
+
ECDHE-RSA-AES256-GCM-SHA384
|
|
63
|
+
ECDHE-ECDSA-AES128-GCM-SHA256
|
|
64
|
+
ECDHE-ECDSA-AES256-GCM-SHA384].join(':')
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return ciphers if ciphers.is_a?(String)
|
|
68
|
+
return ArgumentError, "Ciphers must be Array or String" unless ciphers.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
ciphers.join(':')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def configure_timeouts(credentials)
|
|
74
|
+
default_options.update(
|
|
75
|
+
timeout: credentials.fetch(:timeout, 10),
|
|
76
|
+
open_timeout: credentials.fetch(:open_timeout, 5)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def production?
|
|
81
|
+
if defined? Rails
|
|
82
|
+
Rails.env.production?
|
|
83
|
+
else
|
|
84
|
+
ENV.fetch("RACK_ENV", "development") == 'production'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RobustClientSocket
|
|
2
|
+
module HTTP
|
|
3
|
+
module Helpers
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend(PrivateClassMethods)
|
|
6
|
+
base.private_class_method(*PrivateClassMethods.instance_methods(false))
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module PrivateClassMethods
|
|
11
|
+
MIN_KEY_SIZE = 2048
|
|
12
|
+
|
|
13
|
+
def robust_headers
|
|
14
|
+
{
|
|
15
|
+
'Content-Type' => 'application/json',
|
|
16
|
+
'Accept' => 'application/json',
|
|
17
|
+
'User-Agent' => "RobustClientSocket/#{RobustClientSocket::VERSION}"
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def secure_token
|
|
22
|
+
encrypted_data(app_token)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def encrypted_data(data)
|
|
26
|
+
validate_key_security!
|
|
27
|
+
|
|
28
|
+
encrypted = rsa_key.public_encrypt(
|
|
29
|
+
data,
|
|
30
|
+
OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
::Base64.strict_encode64(encrypted)
|
|
34
|
+
rescue OpenSSL::PKey::RSAError => e
|
|
35
|
+
raise SecurityError, "Encryption failed: #{e.message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rsa_key
|
|
39
|
+
@rsa_key ||= begin
|
|
40
|
+
key = OpenSSL::PKey::RSA.new(public_key)
|
|
41
|
+
raise SecurityError, "Invalid public key" unless key.public?
|
|
42
|
+
key
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_key_security!
|
|
47
|
+
key_bits = rsa_key.n.num_bits
|
|
48
|
+
|
|
49
|
+
if key_bits < MIN_KEY_SIZE
|
|
50
|
+
raise SecurityError,
|
|
51
|
+
"RSA key size (#{key_bits} bits) below minimum (#{MIN_KEY_SIZE} bits)"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def public_key
|
|
56
|
+
credentials[:public_key]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def app_token
|
|
60
|
+
"#{client_name}_#{time_now_in_utc}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def time_now_in_utc
|
|
64
|
+
Time.now.utc.to_i
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RobustClientSocket
|
|
2
|
+
module HTTP
|
|
3
|
+
# This allows us to set dynamic headers every time we make a request
|
|
4
|
+
module HTTPartyOverrides
|
|
5
|
+
DEFAULT_HEADER_NAME = 'Secure-Token'.freeze
|
|
6
|
+
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.singleton_class.prepend(ClassMethods)
|
|
9
|
+
base.private_class_method(*ClassMethods.instance_methods(false))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def perform_request(http_method, path, options, &)
|
|
14
|
+
headers[header_name || DEFAULT_HEADER_NAME] = secure_token
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'robust_client_socket/configuration'
|
|
5
|
+
|
|
6
|
+
module RobustClientSocket
|
|
7
|
+
extend RobustClientSocket::Configuration
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def load! # rubocop:disable Metrics/AbcSize
|
|
12
|
+
raise 'You must configure RobustClientSocket first!' unless configured?
|
|
13
|
+
|
|
14
|
+
require 'openssl'
|
|
15
|
+
require 'httparty'
|
|
16
|
+
require 'base64'
|
|
17
|
+
require 'oj'
|
|
18
|
+
|
|
19
|
+
require_relative 'robust_client_socket/http/httparty_overrides'
|
|
20
|
+
require_relative 'robust_client_socket/http/helpers'
|
|
21
|
+
require_relative 'robust_client_socket/http/client'
|
|
22
|
+
|
|
23
|
+
configuration.services.each do |key, value|
|
|
24
|
+
client = Class.new(RobustClientSocket::HTTP::Client)
|
|
25
|
+
client.init(credentials: value, client_name: configuration.client_name, header_name: configuration.header_name)
|
|
26
|
+
const_set(key.to_s.split('_').map(&:capitalize).join, client)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/version.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "./lib/version.rb"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "robust_client_socket"
|
|
7
|
+
spec.version = RobustClientSocket::VERSION
|
|
8
|
+
spec.authors = ["Taras Zhuk"]
|
|
9
|
+
spec.email = ["tee0zed@gmail.com"]
|
|
10
|
+
spec.summary = "Robust Client Socket"
|
|
11
|
+
spec.description = "Methods that should be used to interact with Robust inner ecosystem."
|
|
12
|
+
spec.required_ruby_version = ">= 2.7.7"
|
|
13
|
+
|
|
14
|
+
# Specify which files should be added to the gem when it is released.
|
|
15
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
16
|
+
spec.files = Dir.chdir(__dir__) do
|
|
17
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
18
|
+
(File.expand_path(f) == __FILE__) ||
|
|
19
|
+
f.start_with?(*%w[features/ .git .circleci Gemfile])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
spec.require_paths = %w[lib config]
|
|
23
|
+
spec.add_dependency 'oj'
|
|
24
|
+
spec.add_dependency 'httparty'
|
|
25
|
+
spec.add_dependency 'rspec'
|
|
26
|
+
spec.add_dependency 'rake'
|
|
27
|
+
|
|
28
|
+
# Uncomment to register a new dependency of your gem
|
|
29
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
|
30
|
+
|
|
31
|
+
# For more information and examples about making a new gem, check out our
|
|
32
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
33
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'httparty'
|
|
3
|
+
require './lib/version.rb'
|
|
4
|
+
require './lib/robust_client_socket/http/helpers'
|
|
5
|
+
require './lib/robust_client_socket/http/httparty_overrides'
|
|
6
|
+
require './lib/robust_client_socket/http/client'
|
|
7
|
+
|
|
8
|
+
RSpec.describe RobustClientSocket::HTTP::Client do
|
|
9
|
+
let(:credentials) do
|
|
10
|
+
{
|
|
11
|
+
base_uri: 'https://example.com',
|
|
12
|
+
public_key: OpenSSL::PKey::RSA.new(2048).public_key.to_s
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
let(:client_name) { 'test_client' }
|
|
16
|
+
let(:header_name) { 'Custom-Token' }
|
|
17
|
+
|
|
18
|
+
after(:each) do
|
|
19
|
+
# Reset HTTParty default_options to avoid test pollution
|
|
20
|
+
described_class.default_options.delete(:verify)
|
|
21
|
+
described_class.default_options.delete(:verify_mode)
|
|
22
|
+
described_class.default_options.delete(:ssl_version)
|
|
23
|
+
described_class.default_options.delete(:ciphers)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '.init' do
|
|
27
|
+
context 'with valid HTTPS credentials' do
|
|
28
|
+
it 'sets credentials' do
|
|
29
|
+
described_class.init(credentials: credentials, client_name: client_name)
|
|
30
|
+
expect(described_class.credentials).to eq(credentials)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'sets client_name' do
|
|
34
|
+
described_class.init(credentials: credentials, client_name: client_name)
|
|
35
|
+
expect(described_class.client_name).to eq(client_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'sets custom header_name' do
|
|
39
|
+
described_class.init(credentials: credentials, client_name: client_name, header_name: header_name)
|
|
40
|
+
expect(described_class.header_name).to eq(header_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'sets base_uri' do
|
|
44
|
+
described_class.init(credentials: credentials, client_name: client_name)
|
|
45
|
+
expect(described_class.base_uri).to eq('https://example.com')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'configures SSL verification when ssl_verify is enabled' do
|
|
49
|
+
ssl_credentials = credentials.merge(ssl_verify: true)
|
|
50
|
+
described_class.init(credentials: ssl_credentials, client_name: client_name)
|
|
51
|
+
expect(described_class.default_options[:verify]).to be true
|
|
52
|
+
expect(described_class.default_options[:verify_mode]).to eq(OpenSSL::SSL::VERIFY_PEER)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'does not configure SSL verification when ssl_verify is not set' do
|
|
56
|
+
described_class.init(credentials: credentials, client_name: client_name)
|
|
57
|
+
expect(described_class.default_options[:verify]).to be_nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'in production with non-HTTPS base_uri and ssl_verify enabled' do
|
|
62
|
+
let(:http_credentials) { credentials.merge(base_uri: 'http://example.com', ssl_verify: true) }
|
|
63
|
+
|
|
64
|
+
before do
|
|
65
|
+
allow(described_class).to receive(:production?).and_return(true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'raises InsecureConnectionError' do
|
|
69
|
+
expect {
|
|
70
|
+
described_class.init(credentials: http_credentials, client_name: client_name)
|
|
71
|
+
}.to raise_error(RobustClientSocket::HTTP::Client::InsecureConnectionError, /HTTPS required in production/)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'in production with non-HTTPS base_uri and ssl_verify disabled' do
|
|
76
|
+
let(:http_credentials) { credentials.merge(base_uri: 'http://example.com', ssl_verify: false) }
|
|
77
|
+
|
|
78
|
+
before do
|
|
79
|
+
allow(described_class).to receive(:production?).and_return(true)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'allows HTTP when ssl_verify is false' do
|
|
83
|
+
expect {
|
|
84
|
+
described_class.init(credentials: http_credentials, client_name: client_name)
|
|
85
|
+
}.not_to raise_error
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context 'in development with HTTP base_uri' do
|
|
90
|
+
let(:http_credentials) { credentials.merge(base_uri: 'http://example.com') }
|
|
91
|
+
|
|
92
|
+
before do
|
|
93
|
+
allow(described_class).to receive(:production?).and_return(false)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'allows HTTP' do
|
|
97
|
+
expect {
|
|
98
|
+
described_class.init(credentials: http_credentials, client_name: client_name)
|
|
99
|
+
}.not_to raise_error
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
RSpec.describe RobustClientSocket::HTTP::Client do
|
|
106
|
+
describe '.production?' do
|
|
107
|
+
context 'when Rails is defined' do
|
|
108
|
+
let(:rails_stub) { double('Rails') }
|
|
109
|
+
let(:rails_env) { double('env', production?: true) }
|
|
110
|
+
|
|
111
|
+
before do
|
|
112
|
+
stub_const('Rails', rails_stub)
|
|
113
|
+
allow(rails_stub).to receive(:env).and_return(rails_env)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'delegates to Rails.production?' do
|
|
117
|
+
expect(described_class.send(:production?)).to be true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
context 'when Rails is not defined' do
|
|
122
|
+
before do
|
|
123
|
+
hide_const('Rails') if defined?(Rails)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'checks RACK_ENV for production' do
|
|
127
|
+
allow(ENV).to receive(:fetch).with('RACK_ENV', 'development').and_return('production')
|
|
128
|
+
expect(described_class.send(:production?)).to be true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'defaults to development' do
|
|
132
|
+
allow(ENV).to receive(:fetch).with('RACK_ENV', 'development').and_return('development')
|
|
133
|
+
expect(described_class.send(:production?)).to be false
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require './lib/robust_client_socket.rb'
|
|
3
|
+
|
|
4
|
+
RSpec.describe RobustClientSocket do
|
|
5
|
+
describe '.load' do
|
|
6
|
+
before do
|
|
7
|
+
RobustClientSocket.configure do |config|
|
|
8
|
+
config.service_one = {
|
|
9
|
+
base_uri: 'https://example1.com',
|
|
10
|
+
public_key: 'public_key'
|
|
11
|
+
}
|
|
12
|
+
config.service_two = {
|
|
13
|
+
base_uri: 'https://example2.com',
|
|
14
|
+
public_key: 'public_key'
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'loads services as separate classes' do
|
|
20
|
+
RobustClientSocket.load!
|
|
21
|
+
expect(RobustClientSocket::ServiceOne).to be_a(Class)
|
|
22
|
+
expect(RobustClientSocket::ServiceTwo).to be_a(Class)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'loads services with correct base_uri' do
|
|
26
|
+
RobustClientSocket.load!
|
|
27
|
+
expect(RobustClientSocket::ServiceOne.base_uri).to eq('https://example1.com')
|
|
28
|
+
expect(RobustClientSocket::ServiceTwo.base_uri).to eq('https://example2.com')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require './lib/robust_client_socket/configuration.rb'
|
|
3
|
+
|
|
4
|
+
RSpec.describe RobustClientSocket::Configuration do
|
|
5
|
+
let(:dummy_class) { Class.new { extend RobustClientSocket::Configuration } }
|
|
6
|
+
|
|
7
|
+
before { allow(dummy_class).to receive(:correct_configuration?).and_return(true) }
|
|
8
|
+
|
|
9
|
+
describe '#configure' do
|
|
10
|
+
let(:public_key) { OpenSSL::PKey::RSA.generate(2048).public_key.to_pem }
|
|
11
|
+
|
|
12
|
+
it 'yields the configuration object to the block' do
|
|
13
|
+
dummy_class.configure do |config|
|
|
14
|
+
config.client_name = 'test'
|
|
15
|
+
config.service = { base_uri: 'https://example.com', public_key: public_key }
|
|
16
|
+
expect(config).to be_a(RobustClientSocket::ConfigStore)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'sets the configured flag to true' do
|
|
21
|
+
dummy_class.configure do |config|
|
|
22
|
+
config.client_name = 'test'
|
|
23
|
+
config.service = { base_uri: 'https://example.com', public_key: public_key }
|
|
24
|
+
end
|
|
25
|
+
expect(dummy_class.configured?).to eq(true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context 'with invalid key size' do
|
|
29
|
+
let(:small_key) { OpenSSL::PKey::RSA.generate(1024).public_key.to_pem }
|
|
30
|
+
|
|
31
|
+
it 'raises SecurityError' do
|
|
32
|
+
expect {
|
|
33
|
+
dummy_class.configure do |config|
|
|
34
|
+
config.client_name = 'test'
|
|
35
|
+
config.service = { base_uri: 'https://example.com', public_key: small_key }
|
|
36
|
+
end
|
|
37
|
+
}.to raise_error(SecurityError, /below minimum/)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#configured?' do
|
|
43
|
+
let(:public_key) { OpenSSL::PKey::RSA.generate(2048).public_key.to_pem }
|
|
44
|
+
|
|
45
|
+
it 'returns false if not configured' do
|
|
46
|
+
expect(dummy_class.configured?).to eq(false)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns true if configured' do
|
|
50
|
+
dummy_class.configure do |config|
|
|
51
|
+
config.client_name = 'test'
|
|
52
|
+
config.service = { base_uri: 'https://example.com', public_key: public_key }
|
|
53
|
+
end
|
|
54
|
+
expect(dummy_class.configured?).to eq(true)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#correct_configuration?' do
|
|
59
|
+
let(:public_key) { OpenSSL::PKey::RSA.generate(2048).public_key.to_pem }
|
|
60
|
+
|
|
61
|
+
before { allow(dummy_class).to receive(:correct_configuration?).and_call_original }
|
|
62
|
+
|
|
63
|
+
it 'returns false if services is empty' do
|
|
64
|
+
dummy_class.configure { |config| }
|
|
65
|
+
expect(dummy_class.correct_configuration?).to eq(false)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns false if creds is missing base_uri' do
|
|
69
|
+
dummy_class.configure { |config| config.service = { public_key: public_key } }
|
|
70
|
+
expect(dummy_class.correct_configuration?).to eq(false)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns false if creds is missing public_key' do
|
|
74
|
+
dummy_class.configure { |config| config.service = { base_uri: 'base_uri' } }
|
|
75
|
+
expect(dummy_class.correct_configuration?).to eq(false)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns true if creds is correct' do
|
|
79
|
+
dummy_class.configure do |config|
|
|
80
|
+
config.client_name = 'sample'
|
|
81
|
+
config.service = { base_uri: 'base_uri', public_key: public_key }
|
|
82
|
+
end
|
|
83
|
+
expect(dummy_class.correct_configuration?).to eq(true)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
RSpec.describe RobustClientSocket::ConfigStore do
|
|
89
|
+
subject(:config_store) { described_class.new }
|
|
90
|
+
|
|
91
|
+
it 'has attribute keychain' do
|
|
92
|
+
config_store.service = { uri: 'uri' }
|
|
93
|
+
expect(config_store.services).to eq({ service: { uri: 'uri' } })
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe '#initialize' do
|
|
97
|
+
it 'initializes services as empty hash' do
|
|
98
|
+
expect(config_store.services).to eq({})
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'initializes client_name as nil' do
|
|
102
|
+
expect(config_store.client_name).to be_nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe '#method_missing' do
|
|
107
|
+
context 'with setter method' do
|
|
108
|
+
it 'adds service to services hash' do
|
|
109
|
+
config_store.api_service = { base_uri: 'https://api.example.com', public_key: 'key' }
|
|
110
|
+
expect(config_store.services[:api_service]).to eq({ base_uri: 'https://api.example.com', public_key: 'key' })
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'rejects non-hash values' do
|
|
114
|
+
config_store.api_service = 'string_value'
|
|
115
|
+
expect(config_store.services[:api_service]).to be false
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe 'accessors' do
|
|
121
|
+
it 'allows setting client_name' do
|
|
122
|
+
config_store.client_name = 'test_client'
|
|
123
|
+
expect(config_store.client_name).to eq('test_client')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'allows setting header_name' do
|
|
127
|
+
config_store.header_name = 'Custom-Header'
|
|
128
|
+
expect(config_store.header_name).to eq('Custom-Header')
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|