castle-rb 3.5.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
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