castle-rb 3.6.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/README.md +157 -0
- data/lib/castle-rb.rb +3 -0
- data/lib/castle.rb +62 -0
- data/lib/castle/api.rb +40 -0
- data/lib/castle/api/request.rb +37 -0
- data/lib/castle/api/request/build.rb +27 -0
- data/lib/castle/api/response.rb +40 -0
- data/lib/castle/client.rb +106 -0
- data/lib/castle/command.rb +5 -0
- data/lib/castle/commands/authenticate.rb +23 -0
- data/lib/castle/commands/identify.rb +23 -0
- data/lib/castle/commands/impersonate.rb +26 -0
- data/lib/castle/commands/review.rb +14 -0
- data/lib/castle/commands/track.rb +23 -0
- data/lib/castle/configuration.rb +80 -0
- data/lib/castle/context/default.rb +40 -0
- data/lib/castle/context/merger.rb +14 -0
- data/lib/castle/context/sanitizer.rb +23 -0
- data/lib/castle/errors.rb +41 -0
- data/lib/castle/extractors/client_id.rb +17 -0
- data/lib/castle/extractors/headers.rb +51 -0
- data/lib/castle/extractors/ip.rb +18 -0
- data/lib/castle/failover_auth_response.rb +21 -0
- data/lib/castle/header_formatter.rb +9 -0
- data/lib/castle/review.rb +11 -0
- data/lib/castle/secure_mode.rb +11 -0
- data/lib/castle/support/hanami.rb +19 -0
- data/lib/castle/support/padrino.rb +19 -0
- data/lib/castle/support/rails.rb +13 -0
- data/lib/castle/support/sinatra.rb +19 -0
- data/lib/castle/utils.rb +55 -0
- data/lib/castle/utils/cloner.rb +11 -0
- data/lib/castle/utils/merger.rb +23 -0
- data/lib/castle/utils/timestamp.rb +12 -0
- data/lib/castle/validators/not_supported.rb +16 -0
- data/lib/castle/validators/present.rb +16 -0
- data/lib/castle/version.rb +5 -0
- data/spec/lib/castle/api/request/build_spec.rb +44 -0
- data/spec/lib/castle/api/request_spec.rb +59 -0
- data/spec/lib/castle/api/response_spec.rb +58 -0
- data/spec/lib/castle/api_spec.rb +37 -0
- data/spec/lib/castle/client_spec.rb +358 -0
- data/spec/lib/castle/command_spec.rb +9 -0
- data/spec/lib/castle/commands/authenticate_spec.rb +108 -0
- data/spec/lib/castle/commands/identify_spec.rb +87 -0
- data/spec/lib/castle/commands/impersonate_spec.rb +106 -0
- data/spec/lib/castle/commands/review_spec.rb +24 -0
- data/spec/lib/castle/commands/track_spec.rb +113 -0
- data/spec/lib/castle/configuration_spec.rb +130 -0
- data/spec/lib/castle/context/default_spec.rb +41 -0
- data/spec/lib/castle/context/merger_spec.rb +23 -0
- data/spec/lib/castle/context/sanitizer_spec.rb +27 -0
- data/spec/lib/castle/extractors/client_id_spec.rb +62 -0
- data/spec/lib/castle/extractors/headers_spec.rb +89 -0
- data/spec/lib/castle/extractors/ip_spec.rb +27 -0
- data/spec/lib/castle/header_formatter_spec.rb +25 -0
- data/spec/lib/castle/review_spec.rb +19 -0
- data/spec/lib/castle/secure_mode_spec.rb +9 -0
- data/spec/lib/castle/utils/cloner_spec.rb +18 -0
- data/spec/lib/castle/utils/merger_spec.rb +13 -0
- data/spec/lib/castle/utils/timestamp_spec.rb +17 -0
- data/spec/lib/castle/utils_spec.rb +156 -0
- data/spec/lib/castle/validators/not_supported_spec.rb +26 -0
- data/spec/lib/castle/validators/present_spec.rb +33 -0
- data/spec/lib/castle/version_spec.rb +5 -0
- data/spec/lib/castle_spec.rb +66 -0
- data/spec/spec_helper.rb +25 -0
- metadata +139 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Hanami
|
5
|
+
module Action
|
6
|
+
def castle
|
7
|
+
@castle ||= ::Castle::Client.from_request(request, cookies: (cookies if defined? cookies))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.configure do
|
13
|
+
controller.prepare do
|
14
|
+
include Castle::Hanami::Action
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Padrino
|
4
|
+
class Application
|
5
|
+
module Castle
|
6
|
+
module Helpers
|
7
|
+
def castle
|
8
|
+
@castle ||= ::Castle::Client.from_request(request)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.registered(app)
|
13
|
+
app.helpers Helpers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
register Castle
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module CastleClient
|
5
|
+
def castle
|
6
|
+
@castle ||= request.env['castle'] || Castle::Client.from_request(request)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
11
|
+
include CastleClient
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
|
5
|
+
module Sinatra
|
6
|
+
module Castle
|
7
|
+
module Helpers
|
8
|
+
def castle
|
9
|
+
@castle ||= ::Castle::Client.from_request(request)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.registered(app)
|
14
|
+
app.helpers Castle::Helpers
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
register Castle
|
19
|
+
end
|
data/lib/castle/utils.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
class << self
|
6
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
7
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
8
|
+
# and from all nested hashes and arrays.
|
9
|
+
#
|
10
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
11
|
+
#
|
12
|
+
# Castle::Hash.deep_symbolize_keys(hash)
|
13
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
14
|
+
def deep_symbolize_keys(object, &block)
|
15
|
+
case object
|
16
|
+
when Hash
|
17
|
+
object.each_with_object({}) do |(key, value), result|
|
18
|
+
result[key.to_sym] = deep_symbolize_keys(value, &block)
|
19
|
+
end
|
20
|
+
when Array
|
21
|
+
object.map { |e| deep_symbolize_keys(e, &block) }
|
22
|
+
else
|
23
|
+
object
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def deep_symbolize_keys!(object, &block)
|
28
|
+
case object
|
29
|
+
when Hash
|
30
|
+
object.keys.each do |key|
|
31
|
+
value = object.delete(key)
|
32
|
+
object[key.to_sym] = deep_symbolize_keys!(value, &block)
|
33
|
+
end
|
34
|
+
object
|
35
|
+
when Array
|
36
|
+
object.map! { |e| deep_symbolize_keys!(e, &block) }
|
37
|
+
else
|
38
|
+
object
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def replace_invalid_characters(arg)
|
43
|
+
if arg.is_a?(::String)
|
44
|
+
arg.encode('UTF-8', invalid: :replace, undef: :replace)
|
45
|
+
elsif arg.is_a?(::Hash)
|
46
|
+
arg.each_with_object({}) { |(k, v), h| h[k] = replace_invalid_characters(v) }
|
47
|
+
elsif arg.is_a?(::Array)
|
48
|
+
arg.map(&method(:replace_invalid_characters))
|
49
|
+
else
|
50
|
+
arg
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Utils
|
5
|
+
class Merger
|
6
|
+
def self.call(base, extra)
|
7
|
+
base_s = Castle::Utils.deep_symbolize_keys(base)
|
8
|
+
extra_s = Castle::Utils.deep_symbolize_keys(extra)
|
9
|
+
|
10
|
+
extra_s.each do |name, value|
|
11
|
+
if value.nil?
|
12
|
+
base_s.delete(name)
|
13
|
+
elsif value.is_a?(Hash) && base_s[name].is_a?(Hash)
|
14
|
+
base_s[name] = call(base_s[name], value)
|
15
|
+
else
|
16
|
+
base_s[name] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
base_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Validators
|
5
|
+
class NotSupported
|
6
|
+
class << self
|
7
|
+
def call(options, keys)
|
8
|
+
keys.each do |key|
|
9
|
+
next unless options.key?(key)
|
10
|
+
raise Castle::InvalidParametersError, "#{key} is/are not supported"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Castle
|
4
|
+
module Validators
|
5
|
+
class Present
|
6
|
+
class << self
|
7
|
+
def call(options, keys)
|
8
|
+
keys.each do |key|
|
9
|
+
next unless options[key].to_s.empty?
|
10
|
+
raise Castle::InvalidParametersError, "#{key} is missing or empty"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Request::Build do
|
4
|
+
subject(:call) { described_class.call(command, headers, api_secret) }
|
5
|
+
|
6
|
+
let(:headers) { { 'SAMPLE-HEADER' => '1' } }
|
7
|
+
let(:api_secret) { 'secret' }
|
8
|
+
|
9
|
+
describe 'call' do
|
10
|
+
context 'when get' do
|
11
|
+
let(:command) { Castle::Commands::Review.build(review_id) }
|
12
|
+
let(:review_id) { SecureRandom.uuid }
|
13
|
+
|
14
|
+
it { expect(call.body).to be_nil }
|
15
|
+
it { expect(call.method).to eql('GET') }
|
16
|
+
it { expect(call.path).to eql("/v1/#{command.path}") }
|
17
|
+
it { expect(call.to_hash).to have_key('authorization') }
|
18
|
+
it { expect(call.to_hash).to have_key('sample-header') }
|
19
|
+
it { expect(call.to_hash['sample-header']).to eql(['1']) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when post' do
|
23
|
+
let(:time) { Time.now.utc.iso8601(3) }
|
24
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded', name: "\xC4") }
|
25
|
+
let(:expected_body) do
|
26
|
+
{
|
27
|
+
event: '$login.succeeded',
|
28
|
+
name: "�",
|
29
|
+
context: {},
|
30
|
+
sent_at: time
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
before { allow(Castle::Utils::Timestamp).to receive(:call).and_return(time) }
|
35
|
+
|
36
|
+
it { expect(call.body).to be_eql(expected_body.to_json) }
|
37
|
+
it { expect(call.method).to eql('POST') }
|
38
|
+
it { expect(call.path).to eql("/v1/#{command.path}") }
|
39
|
+
it { expect(call.to_hash).to have_key('authorization') }
|
40
|
+
it { expect(call.to_hash).to have_key('sample-header') }
|
41
|
+
it { expect(call.to_hash['sample-header']).to eql(['1']) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Request do
|
4
|
+
describe '#call' do
|
5
|
+
subject(:call) { described_class.call(command, api_secret, headers) }
|
6
|
+
|
7
|
+
let(:http) { instance_double('Net::HTTP') }
|
8
|
+
let(:request_build) { instance_double('Castle::API::Request::Build') }
|
9
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
|
10
|
+
let(:headers) { {} }
|
11
|
+
let(:api_secret) { 'secret' }
|
12
|
+
let(:expected_headers) { { 'Content-Type' => 'application/json' } }
|
13
|
+
|
14
|
+
before do
|
15
|
+
allow(described_class).to receive(:http).and_return(http)
|
16
|
+
allow(http).to receive(:request).with(request_build)
|
17
|
+
allow(Castle::API::Request::Build).to receive(:call)
|
18
|
+
.with(command, expected_headers, api_secret)
|
19
|
+
.and_return(request_build)
|
20
|
+
call
|
21
|
+
end
|
22
|
+
|
23
|
+
it do
|
24
|
+
expect(Castle::API::Request::Build).to have_received(:call)
|
25
|
+
.with(command, expected_headers, api_secret)
|
26
|
+
end
|
27
|
+
it { expect(http).to have_received(:request).with(request_build) }
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#http' do
|
31
|
+
subject(:http) { described_class.http }
|
32
|
+
|
33
|
+
context 'when ssl false' do
|
34
|
+
before do
|
35
|
+
Castle.config.host = 'localhost'
|
36
|
+
Castle.config.port = 3002
|
37
|
+
end
|
38
|
+
|
39
|
+
after do
|
40
|
+
Castle.config.host = Castle::Configuration::HOST
|
41
|
+
Castle.config.port = Castle::Configuration::PORT
|
42
|
+
end
|
43
|
+
|
44
|
+
it { expect(http).to be_instance_of(Net::HTTP) }
|
45
|
+
it { expect(http.address).to eq(Castle.config.host) }
|
46
|
+
it { expect(http.port).to eq(Castle.config.port) }
|
47
|
+
it { expect(http.use_ssl?).to be false }
|
48
|
+
it { expect(http.verify_mode).to be_nil }
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when ssl true' do
|
52
|
+
it { expect(http).to be_instance_of(Net::HTTP) }
|
53
|
+
it { expect(http.address).to eq(Castle.config.host) }
|
54
|
+
it { expect(http.port).to eq(Castle.config.port) }
|
55
|
+
it { expect(http.use_ssl?).to be true }
|
56
|
+
it { expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API::Response do
|
4
|
+
describe '#call' do
|
5
|
+
subject(:call) { described_class.call(response) }
|
6
|
+
|
7
|
+
context 'when success' do
|
8
|
+
let(:response) { OpenStruct.new(body: '{"user":1}', code: 200) }
|
9
|
+
|
10
|
+
it { expect(call).to eql(user: 1) }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when response empty' do
|
14
|
+
let(:response) { OpenStruct.new(body: '', code: 200) }
|
15
|
+
|
16
|
+
it { expect(call).to eql({}) }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when response nil' do
|
20
|
+
let(:response) { OpenStruct.new(code: 200) }
|
21
|
+
|
22
|
+
it { expect(call).to eql({}) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when json is malformed' do
|
26
|
+
let(:response) { OpenStruct.new(body: '{a', code: 200) }
|
27
|
+
|
28
|
+
it { expect { call }.to raise_error(Castle::ApiError) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#verify!' do
|
33
|
+
subject(:verify!) { described_class.verify!(response) }
|
34
|
+
|
35
|
+
context 'without error when response is 2xx' do
|
36
|
+
let(:response) { OpenStruct.new(code: 200) }
|
37
|
+
|
38
|
+
it { expect { verify! }.not_to raise_error }
|
39
|
+
end
|
40
|
+
|
41
|
+
shared_examples 'response_failed' do |code, error|
|
42
|
+
let(:response) { OpenStruct.new(code: code) }
|
43
|
+
|
44
|
+
it "fail when response is #{code}" do
|
45
|
+
expect { verify! }.to raise_error(error)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it_behaves_like 'response_failed', '400', Castle::BadRequestError
|
50
|
+
it_behaves_like 'response_failed', '401', Castle::UnauthorizedError
|
51
|
+
it_behaves_like 'response_failed', '403', Castle::ForbiddenError
|
52
|
+
it_behaves_like 'response_failed', '404', Castle::NotFoundError
|
53
|
+
it_behaves_like 'response_failed', '419', Castle::UserUnauthorizedError
|
54
|
+
it_behaves_like 'response_failed', '422', Castle::InvalidParametersError
|
55
|
+
it_behaves_like 'response_failed', '499', Castle::ApiError
|
56
|
+
it_behaves_like 'response_failed', '500', Castle::InternalServerError
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Castle::API do
|
4
|
+
subject(:request) { described_class.request(command) }
|
5
|
+
|
6
|
+
let(:command) { Castle::Commands::Track.new({}).build(event: '$login.succeeded') }
|
7
|
+
|
8
|
+
context 'when request timeouts' do
|
9
|
+
before { stub_request(:any, /api.castle.io/).to_timeout }
|
10
|
+
|
11
|
+
it do
|
12
|
+
expect do
|
13
|
+
request
|
14
|
+
end.to raise_error(Castle::RequestError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when non-OK response code' do
|
19
|
+
before { stub_request(:any, /api.castle.io/).to_return(status: 400) }
|
20
|
+
|
21
|
+
it do
|
22
|
+
expect do
|
23
|
+
request
|
24
|
+
end.to raise_error(Castle::BadRequestError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when no api_secret' do
|
29
|
+
before { allow(Castle.config).to receive(:api_secret).and_return('') }
|
30
|
+
|
31
|
+
it do
|
32
|
+
expect do
|
33
|
+
request
|
34
|
+
end.to raise_error(Castle::ConfigurationError)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|