castle-rb 3.5.0 → 3.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6376098ba7a5b711d926fe187c38d2ed31a68b7ddf53024f2d51a81a14482604
4
- data.tar.gz: b4ca1f4dbd10607b5c5bfad6a9b7528d3ebb8ebf7a2b6fd1287af08333b110d4
3
+ metadata.gz: a710c22d8a912a0918f42fe092f6c3b883a4a03ad7e2b86572b17b6ee901931e
4
+ data.tar.gz: e7325e9e957881ddcee78729d6274805d29b62c1f468e26fd03d34cf88286ac3
5
5
  SHA512:
6
- metadata.gz: 70c04aa5f1d0f3f4af5789a7aad877380b3d82fafbd935ec746bd40aab904c110bea4410d161c48521fee108a5212c7d81b8a00b465b82940e7f6f9c48236f26
7
- data.tar.gz: 345383199ba415d699f97f1ebf242d21dc8e1eb921a4c60f6366464a1e6bc5b5ac0680d7010e6f2b4b9ff5366393c9fd9b70237951ac7b013328e05af3af11b7
6
+ metadata.gz: 84f69693b63c3c52b3084348aba0bc5e28e047f6f56a5a756559abf895e8031d8c33df06f385c6383c62bcaa240dfb1b8d3550dc7bb6f9b3872b9dd1bf7851ae
7
+ data.tar.gz: 24ecbd126fa6bb601acf00c442d8204c6042c87b9c47709af42e639e40409b70feca5c06f1c0fab50d7c9d0eb5ba20390229c6f7b6c74f170ab1918b5dec3b03
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
  [![Build Status](https://travis-ci.org/castle/castle-ruby.svg?branch=master)](https://travis-ci.org/castle/castle-ruby)
4
4
  [![Coverage Status](https://coveralls.io/repos/github/castle/castle-ruby/badge.svg?branch=coveralls)](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
5
5
  [![Gem Version](https://badge.fury.io/rb/castle-rb.svg)](https://badge.fury.io/rb/castle-rb)
6
- [![Dependency Status](https://gemnasium.com/badges/github.com/castle/castle-ruby.svg)](https://gemnasium.com/github.com/castle/castle-ruby)
7
6
 
8
7
  **[Castle](https://castle.io) analyzes device, location, and interaction patterns in your web and mobile apps and lets you stop account takeover attacks in real-time..**
9
8
 
@@ -1,39 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'openssl'
4
- require 'net/http'
5
- require 'json'
6
- require 'time'
3
+ %w[
4
+ openssl
5
+ net/http
6
+ json
7
+ time
8
+ ].each(&method(:require))
7
9
 
8
- require 'castle/version'
9
- require 'castle/errors'
10
- require 'castle/command'
11
- require 'castle/utils'
12
- require 'castle/utils/merger'
13
- require 'castle/utils/cloner'
14
- require 'castle/utils/timestamp'
15
- require 'castle/validators/present'
16
- require 'castle/validators/not_supported'
17
- require 'castle/context/merger'
18
- require 'castle/context/sanitizer'
19
- require 'castle/context/default'
20
- require 'castle/commands/identify'
21
- require 'castle/commands/authenticate'
22
- require 'castle/commands/track'
23
- require 'castle/commands/review'
24
- require 'castle/commands/impersonate'
25
- require 'castle/configuration'
26
- require 'castle/failover_auth_response'
27
- require 'castle/client'
28
- require 'castle/header_formatter'
29
- require 'castle/secure_mode'
30
- require 'castle/extractors/client_id'
31
- require 'castle/extractors/headers'
32
- require 'castle/extractors/ip'
33
- require 'castle/response'
34
- require 'castle/request'
35
- require 'castle/review'
36
- require 'castle/api'
10
+ %w[
11
+ castle/version
12
+ castle/errors
13
+ castle/command
14
+ castle/utils
15
+ castle/utils/merger
16
+ castle/utils/cloner
17
+ castle/utils/timestamp
18
+ castle/validators/present
19
+ castle/validators/not_supported
20
+ castle/context/merger
21
+ castle/context/sanitizer
22
+ castle/context/default
23
+ castle/commands/identify
24
+ castle/commands/authenticate
25
+ castle/commands/track
26
+ castle/commands/review
27
+ castle/commands/impersonate
28
+ castle/configuration
29
+ castle/failover_auth_response
30
+ castle/client
31
+ castle/header_formatter
32
+ castle/secure_mode
33
+ castle/extractors/client_id
34
+ castle/extractors/headers
35
+ castle/extractors/ip
36
+ castle/api/response
37
+ castle/api/request
38
+ castle/api/request/build
39
+ castle/review
40
+ castle/api
41
+ ].each(&method(:require))
37
42
 
38
43
  # main sdk module
39
44
  module Castle
@@ -2,44 +2,38 @@
2
2
 
3
3
  module Castle
4
4
  # this class is responsible for making requests to api
5
- class API
6
- def initialize(headers = {})
7
- @config = Castle.config
8
- @http = prepare_http
9
- @headers = headers.merge('Content-Type' => 'application/json')
10
- end
11
-
12
- def request(command)
13
- request = Castle::Request.new(@headers).build(
14
- command.path,
15
- command.data,
16
- command.method
17
- )
18
- perform_request(request)
19
- end
5
+ module API
6
+ # Errors we handle internally
7
+ HANDLED_ERRORS = [
8
+ Timeout::Error,
9
+ Errno::EINVAL,
10
+ Errno::ECONNRESET,
11
+ EOFError,
12
+ Net::HTTPBadResponse,
13
+ Net::HTTPHeaderSyntaxError,
14
+ Net::ProtocolError
15
+ ].freeze
20
16
 
21
- private
17
+ private_constant :HANDLED_ERRORS
22
18
 
23
- def prepare_http
24
- http = Net::HTTP.new(@config.host, @config.port)
25
- http.read_timeout = @config.request_timeout / 1000.0
26
- prepare_http_for_ssl(http) if @config.port == 443
27
- http
28
- end
29
-
30
- def prepare_http_for_ssl(http)
31
- http.use_ssl = true
32
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
33
- end
19
+ class << self
20
+ def request(command, headers = {})
21
+ raise Castle::ConfigurationError, 'configuration is not valid' unless Castle.config.valid?
34
22
 
35
- def perform_request(request)
36
- raise Castle::ConfigurationError, 'configuration is not valid' unless @config.valid?
37
- begin
38
- Castle::Response.new(@http.request(request)).parse
39
- rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
40
- Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
41
- Net::ProtocolError
42
- raise Castle::RequestError, 'Castle API connection error'
23
+ begin
24
+ Castle::API::Response.call(
25
+ Castle::API::Request.call(
26
+ command,
27
+ Castle.config.api_secret,
28
+ headers
29
+ )
30
+ )
31
+ rescue *HANDLED_ERRORS => error
32
+ # @note We need to initialize the error, as the original error is a cause for this
33
+ # custom exception. If we would do it the default Ruby way, the original error
34
+ # would get converted into a string
35
+ raise Castle::RequestError.new(error) # rubocop:disable Style/RaiseArgs
36
+ end
43
37
  end
44
38
  end
45
39
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # this class is responsible for making requests to api
5
+ module API
6
+ module Request
7
+ # Default headers that we add to passed ones
8
+ DEFAULT_HEADERS = {
9
+ 'Content-Type' => 'application/json'
10
+ }.freeze
11
+
12
+ private_constant :DEFAULT_HEADERS
13
+
14
+ class << self
15
+ def call(command, api_secret, headers)
16
+ http.request(
17
+ Castle::API::Request::Build.call(
18
+ command,
19
+ headers.merge(DEFAULT_HEADERS),
20
+ api_secret
21
+ )
22
+ )
23
+ end
24
+
25
+ def http
26
+ http = Net::HTTP.new(Castle.config.host, Castle.config.port)
27
+ http.read_timeout = Castle.config.request_timeout / 1000.0
28
+ if Castle.config.port == 443
29
+ http.use_ssl = true
30
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
31
+ end
32
+ http
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # generate api request
6
+ module Request
7
+ module Build
8
+ class << self
9
+ def call(command, headers, api_secret)
10
+ request = Net::HTTP.const_get(
11
+ command.method.to_s.capitalize
12
+ ).new("/#{Castle.config.url_prefix}/#{command.path}", headers)
13
+
14
+ unless command.method == :get
15
+ request.body = ::Castle::Utils.replace_invalid_characters(
16
+ command.data
17
+ ).to_json
18
+ end
19
+
20
+ request.basic_auth('', api_secret)
21
+ request
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # parses api response
6
+ module Response
7
+ RESPONSE_ERRORS = {
8
+ 400 => Castle::BadRequestError,
9
+ 401 => Castle::UnauthorizedError,
10
+ 403 => Castle::ForbiddenError,
11
+ 404 => Castle::NotFoundError,
12
+ 419 => Castle::UserUnauthorizedError,
13
+ 422 => Castle::InvalidParametersError
14
+ }.freeze
15
+
16
+ class << self
17
+ def call(response)
18
+ verify!(response)
19
+
20
+ return {} if response.body.nil? || response.body.empty?
21
+
22
+ begin
23
+ JSON.parse(response.body, symbolize_names: true)
24
+ rescue JSON::ParserError
25
+ raise Castle::ApiError, 'Invalid response from Castle API'
26
+ end
27
+ end
28
+
29
+ def verify!(response)
30
+ return if response.code.to_i.between?(200, 299)
31
+
32
+ raise Castle::InternalServerError if response.code.to_i.between?(500, 599)
33
+
34
+ error = RESPONSE_ERRORS.fetch(response.code.to_i, Castle::ApiError)
35
+ raise error, response[:message]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -27,13 +27,12 @@ module Castle
27
27
  end
28
28
  end
29
29
 
30
- attr_accessor :api
30
+ attr_accessor :context
31
31
 
32
32
  def initialize(context, options = {})
33
33
  @do_not_track = options.fetch(:do_not_track, false)
34
34
  @timestamp = options[:timestamp]
35
35
  @context = context
36
- @api = API.new
37
36
  end
38
37
 
39
38
  def authenticate(options = {})
@@ -43,7 +42,7 @@ module Castle
43
42
  add_timestamp_if_necessary(options)
44
43
  command = Castle::Commands::Authenticate.new(@context).build(options)
45
44
  begin
46
- @api.request(command).merge(failover: false, failover_reason: nil)
45
+ Castle::API.request(command).merge(failover: false, failover_reason: nil)
47
46
  rescue Castle::RequestError, Castle::InternalServerError => error
48
47
  self.class.failover_response_or_raise(
49
48
  FailoverAuthResponse.new(options[:user_id], reason: error.to_s), error
@@ -64,7 +63,7 @@ module Castle
64
63
  add_timestamp_if_necessary(options)
65
64
 
66
65
  command = Castle::Commands::Identify.new(@context).build(options)
67
- @api.request(command)
66
+ Castle::API.request(command)
68
67
  end
69
68
 
70
69
  def track(options = {})
@@ -74,14 +73,14 @@ module Castle
74
73
  add_timestamp_if_necessary(options)
75
74
 
76
75
  command = Castle::Commands::Track.new(@context).build(options)
77
- @api.request(command)
76
+ Castle::API.request(command)
78
77
  end
79
78
 
80
79
  def impersonate(options = {})
81
80
  options = Castle::Utils.deep_symbolize_keys(options || {})
82
81
  add_timestamp_if_necessary(options)
83
82
  command = Castle::Commands::Impersonate.new(@context).build(options)
84
- @api.request(command).tap do |response|
83
+ Castle::API.request(command).tap do |response|
85
84
  raise Castle::ImpersonationFailed unless response[:success]
86
85
  end
87
86
  end
@@ -3,6 +3,10 @@
3
3
  module Castle
4
4
  # manages configuration variables
5
5
  class Configuration
6
+ HOST = 'api.castle.io'
7
+ PORT = 443
8
+ URL_PREFIX = 'v1'
9
+ FAILOVER_STRATEGY = :allow
6
10
  REQUEST_TIMEOUT = 500 # in milliseconds
7
11
  FAILOVER_STRATEGIES = %i[allow deny challenge throw].freeze
8
12
  WHITELISTED = [
@@ -20,7 +24,6 @@ module Castle
20
24
  'X-Forwarded-For',
21
25
  'CF_CONNECTING_IP'
22
26
  ].freeze
23
-
24
27
  BLACKLISTED = ['HTTP_COOKIE'].freeze
25
28
 
26
29
  attr_accessor :host, :port, :request_timeout, :url_prefix
@@ -29,10 +32,10 @@ module Castle
29
32
  def initialize
30
33
  @formatter = Castle::HeaderFormatter.new
31
34
  @request_timeout = REQUEST_TIMEOUT
32
- self.failover_strategy = :allow
33
- self.host = 'api.castle.io'
34
- self.port = 443
35
- self.url_prefix = 'v1'
35
+ self.failover_strategy = FAILOVER_STRATEGY
36
+ self.host = HOST
37
+ self.port = PORT
38
+ self.url_prefix = URL_PREFIX
36
39
  self.whitelisted = WHITELISTED
37
40
  self.blacklisted = BLACKLISTED
38
41
  self.api_secret = ''
@@ -3,8 +3,17 @@
3
3
  module Castle
4
4
  # general error
5
5
  class Error < RuntimeError; end
6
- # request error
7
- class RequestError < Castle::Error; end
6
+ # Raised when anything is wrong with the request (any unhappy path)
7
+ # This error indicates that either we would wait too long for a response or something
8
+ # else happened somewhere in the middle and we weren't able to get the results
9
+ class RequestError < Castle::Error
10
+ attr_reader :reason
11
+
12
+ # @param reason [Exception] the core exception that causes this error
13
+ def initialize(reason)
14
+ @reason = reason
15
+ end
16
+ end
8
17
  # security error
9
18
  class SecurityError < Castle::Error; end
10
19
  # wrong configuration error
@@ -3,9 +3,9 @@
3
3
  module Castle
4
4
  class Review
5
5
  def self.retrieve(review_id)
6
- command = Castle::Commands::Review.build(review_id)
7
-
8
- API.new.request(command)
6
+ Castle::API.request(
7
+ Castle::Commands::Review.build(review_id)
8
+ )
9
9
  end
10
10
  end
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Castle
4
- VERSION = '3.5.0'
4
+ VERSION = '3.5.1'
5
5
  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
@@ -1,44 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe Castle::API do
4
- let(:api) { described_class.new('X-Castle-Client-Id' => 'abcd', 'X-Castle-Ip' => '1.2.3.4') }
5
- let(:command) { Castle::Command.new('authenticate', '1234', :post) }
6
- let(:result_headers) do
7
- { 'Accept' => '*/*',
8
- 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
9
- 'User-Agent' => 'Ruby', 'X-Castle-Client-Id' => 'abcd',
10
- 'X-Castle-Ip' => '1.2.3.4' }
11
- end
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 }
12
10
 
13
- describe 'handles timeout' do
14
- before do
15
- stub_request(:any, /api.castle.io/).to_timeout
16
- end
17
11
  it do
18
12
  expect do
19
- api.request(command)
13
+ request
20
14
  end.to raise_error(Castle::RequestError)
21
15
  end
22
16
  end
23
17
 
24
- describe 'handles non-OK response code' do
25
- before do
26
- stub_request(:any, /api.castle.io/).to_return(status: 400)
27
- end
18
+ context 'when non-OK response code' do
19
+ before { stub_request(:any, /api.castle.io/).to_return(status: 400) }
20
+
28
21
  it do
29
22
  expect do
30
- api.request(command)
23
+ request
31
24
  end.to raise_error(Castle::BadRequestError)
32
25
  end
33
26
  end
34
27
 
35
- describe 'handles missing configuration' do
36
- before do
37
- allow(Castle.config).to receive(:api_secret).and_return('')
38
- end
28
+ context 'when no api_secret' do
29
+ before { allow(Castle.config).to receive(:api_secret).and_return('') }
30
+
39
31
  it do
40
32
  expect do
41
- api.request(command)
33
+ request
42
34
  end.to raise_error(Castle::ConfigurationError)
43
35
  end
44
36
  end
@@ -50,12 +50,12 @@ describe Castle::Client do
50
50
 
51
51
  describe 'parses the request' do
52
52
  before do
53
- allow(Castle::API).to receive(:new).and_call_original
53
+ allow(Castle::API).to receive(:request).and_call_original
54
54
  end
55
55
 
56
56
  it do
57
57
  client.authenticate(event: '$login.succeeded', user_id: '1234')
58
- expect(Castle::API).to have_received(:new)
58
+ expect(Castle::API).to have_received(:request)
59
59
  end
60
60
  end
61
61
 
@@ -244,7 +244,7 @@ describe Castle::Client do
244
244
  end
245
245
 
246
246
  context 'when request with fail' do
247
- before { allow(client.api).to receive(:request).and_raise(Castle::RequestError) }
247
+ before { allow(Castle::API).to receive(:request).and_raise(Castle::RequestError.new(Timeout::Error)) }
248
248
 
249
249
  context 'with request error and throw strategy' do
250
250
  before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
@@ -262,7 +262,7 @@ describe Castle::Client do
262
262
  end
263
263
 
264
264
  context 'when request is internal server error' do
265
- before { allow(client.api).to receive(:request).and_raise(Castle::InternalServerError) }
265
+ before { allow(Castle::API).to receive(:request).and_raise(Castle::InternalServerError) }
266
266
 
267
267
  describe 'throw strategy' do
268
268
  before { allow(Castle.config).to receive(:failover_strategy).and_return(:throw) }
@@ -14,10 +14,6 @@ describe Castle::Review do
14
14
 
15
15
  before { retrieve }
16
16
 
17
- it do
18
- assert_requested :get,
19
- "https://api.castle.io/v1/reviews/#{review_id}",
20
- times: 1
21
- end
17
+ it { assert_requested :get, "https://api.castle.io/v1/reviews/#{review_id}", times: 1 }
22
18
  end
23
19
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: castle-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.0
4
+ version: 3.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johan Brissmyr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-18 00:00:00.000000000 Z
11
+ date: 2018-10-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Castle protects your users from account compromise
14
14
  email: johan@castle.io
@@ -20,6 +20,9 @@ files:
20
20
  - lib/castle-rb.rb
21
21
  - lib/castle.rb
22
22
  - lib/castle/api.rb
23
+ - lib/castle/api/request.rb
24
+ - lib/castle/api/request/build.rb
25
+ - lib/castle/api/response.rb
23
26
  - lib/castle/client.rb
24
27
  - lib/castle/command.rb
25
28
  - lib/castle/commands/authenticate.rb
@@ -37,8 +40,6 @@ files:
37
40
  - lib/castle/extractors/ip.rb
38
41
  - lib/castle/failover_auth_response.rb
39
42
  - lib/castle/header_formatter.rb
40
- - lib/castle/request.rb
41
- - lib/castle/response.rb
42
43
  - lib/castle/review.rb
43
44
  - lib/castle/secure_mode.rb
44
45
  - lib/castle/support/hanami.rb
@@ -52,6 +53,9 @@ files:
52
53
  - lib/castle/validators/not_supported.rb
53
54
  - lib/castle/validators/present.rb
54
55
  - lib/castle/version.rb
56
+ - spec/lib/castle/api/request/build_spec.rb
57
+ - spec/lib/castle/api/request_spec.rb
58
+ - spec/lib/castle/api/response_spec.rb
55
59
  - spec/lib/castle/api_spec.rb
56
60
  - spec/lib/castle/client_spec.rb
57
61
  - spec/lib/castle/command_spec.rb
@@ -68,8 +72,6 @@ files:
68
72
  - spec/lib/castle/extractors/headers_spec.rb
69
73
  - spec/lib/castle/extractors/ip_spec.rb
70
74
  - spec/lib/castle/header_formatter_spec.rb
71
- - spec/lib/castle/request_spec.rb
72
- - spec/lib/castle/response_spec.rb
73
75
  - spec/lib/castle/review_spec.rb
74
76
  - spec/lib/castle/secure_mode_spec.rb
75
77
  - spec/lib/castle/utils/cloner_spec.rb
@@ -101,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
103
  version: '0'
102
104
  requirements: []
103
105
  rubyforge_project:
104
- rubygems_version: 2.7.4
106
+ rubygems_version: 2.7.6
105
107
  signing_key:
106
108
  specification_version: 4
107
109
  summary: Castle
@@ -121,8 +123,9 @@ test_files:
121
123
  - spec/lib/castle/utils/timestamp_spec.rb
122
124
  - spec/lib/castle/utils/merger_spec.rb
123
125
  - spec/lib/castle/command_spec.rb
124
- - spec/lib/castle/request_spec.rb
125
- - spec/lib/castle/response_spec.rb
126
+ - spec/lib/castle/api/request_spec.rb
127
+ - spec/lib/castle/api/response_spec.rb
128
+ - spec/lib/castle/api/request/build_spec.rb
126
129
  - spec/lib/castle/commands/review_spec.rb
127
130
  - spec/lib/castle/commands/authenticate_spec.rb
128
131
  - spec/lib/castle/commands/track_spec.rb
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # generate api request
5
- class Request
6
- def initialize(headers = {})
7
- @config = Castle.config
8
- @headers = headers
9
- end
10
-
11
- def build(endpoint, args, method)
12
- request = Net::HTTP.const_get(method.to_s.capitalize).new(build_url(endpoint), @headers)
13
- request.body = ::Castle::Utils.replace_invalid_characters(args).to_json unless method == :get
14
- add_basic_auth(request)
15
- request
16
- end
17
-
18
- private
19
-
20
- def build_url(endpoint)
21
- "/#{@config.url_prefix}/#{endpoint}"
22
- end
23
-
24
- def add_basic_auth(request)
25
- request.basic_auth('', @config.api_secret)
26
- end
27
- end
28
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Castle
4
- # parses api response
5
- class Response
6
- RESPONSE_ERRORS = {
7
- 400 => Castle::BadRequestError,
8
- 401 => Castle::UnauthorizedError,
9
- 403 => Castle::ForbiddenError,
10
- 404 => Castle::NotFoundError,
11
- 419 => Castle::UserUnauthorizedError,
12
- 422 => Castle::InvalidParametersError
13
- }.freeze
14
-
15
- def initialize(response)
16
- @response = response
17
- verify_response_code
18
- end
19
-
20
- def parse
21
- response_body = @response.body
22
-
23
- return {} if response_body.nil? || response_body.empty?
24
-
25
- begin
26
- JSON.parse(response_body, symbolize_names: true)
27
- rescue JSON::ParserError
28
- raise Castle::ApiError, 'Invalid response from Castle API'
29
- end
30
- end
31
-
32
- private
33
-
34
- def verify_response_code
35
- response_code = @response.code.to_i
36
-
37
- return if response_code.between?(200, 299)
38
-
39
- raise Castle::InternalServerError if response_code.between?(500, 599)
40
-
41
- error = RESPONSE_ERRORS.fetch(response_code, Castle::ApiError)
42
- raise error, @response[:message]
43
- end
44
- end
45
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Request do
4
- subject { described_class.new(headers) }
5
-
6
- let(:headers) { { 'SAMPLE-HEADER' => '1' } }
7
- let(:api_secret) { Castle.config.api_secret }
8
-
9
- describe 'build' do
10
- context 'when get' do
11
- let(:path) { 'endpoint' }
12
- let(:params) { { user_id: 1 } }
13
- let(:request) { subject.build(path, params, :get) }
14
-
15
- it { expect(request.body).to be_nil }
16
- it { expect(request.method).to eql('GET') }
17
- it { expect(request.path).to eql('/v1/endpoint') }
18
- it { expect(request.to_hash['sample-header']).to eql(['1']) }
19
- it { expect(request.to_hash['authorization'][0]).to match(/Basic \w/) }
20
- end
21
-
22
- context 'when post' do
23
- let(:path) { 'endpoint' }
24
- let(:params) { { user_id: 1 } }
25
- let(:request) { subject.build(path, params, :post) }
26
-
27
- it { expect(request.body).to be_eql('{"user_id":1}') }
28
- it { expect(request.method).to eql('POST') }
29
- it { expect(request.path).to eql('/v1/endpoint') }
30
- it { expect(request.to_hash['sample-header']).to eql(['1']) }
31
- it { expect(request.to_hash['authorization'][0]).to match(/Basic \w/) }
32
-
33
- context 'with non-UTF-8 charaters' do
34
- let(:params) { { name: "\xC4" } }
35
-
36
- it { expect(request.body).to eq '{"name":"�"}' }
37
- end
38
- end
39
- end
40
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- describe Castle::Response do
4
- subject(:castle_response) do
5
- described_class.new(response)
6
- end
7
-
8
- describe 'initialize' do
9
- context 'without error when response is 2xx' do
10
- let(:response) { OpenStruct.new(code: 200) }
11
-
12
- it do
13
- expect(castle_response).to be_truthy
14
- end
15
- end
16
-
17
- shared_examples 'response_failed' do |code, error|
18
- let(:response) { OpenStruct.new(code: code) }
19
-
20
- it "fail when response is #{code}" do
21
- expect do
22
- castle_response
23
- end.to raise_error(error)
24
- end
25
- end
26
-
27
- it_behaves_like 'response_failed', 400, Castle::BadRequestError
28
- it_behaves_like 'response_failed', 401, Castle::UnauthorizedError
29
- it_behaves_like 'response_failed', 403, Castle::ForbiddenError
30
- it_behaves_like 'response_failed', 404, Castle::NotFoundError
31
- it_behaves_like 'response_failed', 419, Castle::UserUnauthorizedError
32
- it_behaves_like 'response_failed', 422, Castle::InvalidParametersError
33
- it_behaves_like 'response_failed', 499, Castle::ApiError
34
- it_behaves_like 'response_failed', 500, Castle::InternalServerError
35
- end
36
-
37
- describe 'parse' do
38
- context 'when success' do
39
- let(:response) { OpenStruct.new(body: '{"user":1}', code: 200) }
40
-
41
- it do
42
- expect(castle_response.parse).to eql(user: 1)
43
- end
44
- end
45
- context 'when response empty' do
46
- let(:response) { OpenStruct.new(body: '', code: 200) }
47
-
48
- it do
49
- expect(castle_response.parse).to eql({})
50
- end
51
- end
52
- context 'when response nil' do
53
- let(:response) { OpenStruct.new(code: 200) }
54
-
55
- it do
56
- expect(castle_response.parse).to eql({})
57
- end
58
- end
59
- context 'when json is malformed' do
60
- let(:response) { OpenStruct.new(body: '{a', code: 200) }
61
-
62
- it do
63
- expect do
64
- castle_response.parse
65
- end.to raise_error(Castle::ApiError)
66
- end
67
- end
68
- end
69
- end