castle-rb 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +2 -5
  2. data/README.md +1 -1
  3. data/lib/castle-rb.rb +2 -19
  4. data/lib/castle.rb +42 -0
  5. data/lib/castle/api.rb +48 -0
  6. data/lib/castle/client.rb +43 -0
  7. data/lib/castle/configuration.rb +39 -0
  8. data/lib/{castle-rb/support → castle}/cookie_store.rb +9 -5
  9. data/lib/castle/errors.rb +27 -0
  10. data/lib/castle/extractors/client_id.rb +28 -0
  11. data/lib/castle/extractors/headers.rb +36 -0
  12. data/lib/castle/extractors/ip.rb +16 -0
  13. data/lib/castle/headers.rb +39 -0
  14. data/lib/castle/request.rb +34 -0
  15. data/lib/castle/response.rb +43 -0
  16. data/lib/castle/support.rb +11 -0
  17. data/lib/{castle-rb → castle}/support/padrino.rb +1 -1
  18. data/lib/{castle-rb → castle}/support/rails.rb +2 -0
  19. data/lib/{castle-rb → castle}/support/sinatra.rb +1 -1
  20. data/lib/castle/system.rb +36 -0
  21. data/lib/castle/version.rb +5 -0
  22. data/spec/lib/castle/api_spec.rb +54 -0
  23. data/spec/lib/castle/client_spec.rb +64 -0
  24. data/spec/lib/castle/configuration_spec.rb +98 -0
  25. data/spec/lib/castle/extractors/client_id_spec.rb +39 -0
  26. data/spec/lib/castle/extractors/headers_spec.rb +25 -0
  27. data/spec/lib/castle/extractors/ip_spec.rb +20 -0
  28. data/spec/lib/castle/headers_spec.rb +82 -0
  29. data/spec/lib/castle/request_spec.rb +37 -0
  30. data/spec/lib/castle/response_spec.rb +71 -0
  31. data/spec/lib/castle/system_spec.rb +70 -0
  32. data/spec/lib/castle/version_spec.rb +9 -0
  33. data/spec/lib/castle_spec.rb +73 -0
  34. data/spec/spec_helper.rb +9 -3
  35. metadata +49 -76
  36. data/lib/castle-rb/api.rb +0 -94
  37. data/lib/castle-rb/client.rb +0 -67
  38. data/lib/castle-rb/configuration.rb +0 -48
  39. data/lib/castle-rb/errors.rb +0 -15
  40. data/lib/castle-rb/version.rb +0 -3
  41. data/spec/api_spec.rb +0 -31
  42. data/spec/client_spec.rb +0 -55
@@ -0,0 +1,43 @@
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
+ error = RESPONSE_ERRORS.fetch(response_code, Castle::ApiError)
40
+ raise error, @response[:message]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'castle/support/rails' if defined?(Rails::Railtie)
4
+
5
+ if defined?(Sinatra::Base)
6
+ if defined?(Padrino)
7
+ require 'castle/support/padrino'
8
+ else
9
+ require 'castle/support/sinatra'
10
+ end
11
+ end
@@ -1,4 +1,4 @@
1
- require_relative 'cookie_store'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Padrino
4
4
  class Application
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Castle
2
4
  module CastleClient
3
5
  def castle
@@ -1,4 +1,4 @@
1
- require_relative 'cookie_store'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sinatra
4
4
  module Castle
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ # Get information regarding system
5
+ module System
6
+ class << self
7
+ # Returns hardware name, nodename, operating system release,
8
+ # name and version
9
+ # @example Castle::System.uname #=>
10
+ # Linux server 3.18.44-vs2.3.7.5-beng #1 SMP
11
+ # Thu Oct 27 14:11:29 BST 2016 x86_64 GNU/Linux
12
+ def uname
13
+ `uname -a 2>/dev/null`.strip if platform =~ /linux|darwin/i
14
+ rescue Errno::ENOMEM # couldn't create subprocess
15
+ 'uname lookup failed'
16
+ end
17
+
18
+ # Returns current system platform
19
+ # @example Castle::System.platform #=> 'x86_64-pc-linux-gnu'
20
+ def platform
21
+ begin
22
+ require 'rbconfig'
23
+ RbConfig::CONFIG['host'] || RUBY_PLATFORM
24
+ rescue LoadError
25
+ RUBY_PLATFORM
26
+ end.downcase
27
+ end
28
+
29
+ # Returns ruby version
30
+ # @example Castle::System.ruby_version #=> '2.4.1-p111 (2017-03-22)'
31
+ def ruby_version
32
+ "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ VERSION = '2.3.0'
5
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::API do
6
+ let(:api) { described_class.new('abcd', '1.2.3.4', '{}') }
7
+ let(:api_endpoint) { 'http://new.herokuapp.com:3000/v2' }
8
+
9
+ describe 'handles timeout' do
10
+ before do
11
+ stub_request(:any, /api.castle.io/).to_timeout
12
+ end
13
+ it do
14
+ expect do
15
+ api.request('authenticate', user_id: '1234')
16
+ end.to raise_error(Castle::RequestError)
17
+ end
18
+ end
19
+
20
+ describe 'handles non-OK response code' do
21
+ before do
22
+ stub_request(:any, /api.castle.io/).to_return(status: 400)
23
+ end
24
+ it 'handles non-OK response code' do
25
+ expect do
26
+ api.request('authenticate', user_id: '1234')
27
+ end.to raise_error(Castle::BadRequestError)
28
+ end
29
+ end
30
+
31
+ describe 'handles custom API endpoint' do
32
+ before do
33
+ stub_request(:any, /new.herokuapp.com/)
34
+ Castle.config.api_endpoint = api_endpoint
35
+ end
36
+ it do
37
+ api.request('authenticate', user_id: '1234')
38
+ path = "#{api_endpoint.gsub(/new/, ':secret@new')}/authenticate"
39
+ assert_requested :post, path, times: 1
40
+ end
41
+ end
42
+
43
+ describe 'handles query request' do
44
+ before do
45
+ stub_request(:any, /new.herokuapp.com/)
46
+ Castle.config.api_endpoint = api_endpoint
47
+ end
48
+ it do
49
+ api.request_query('review/1')
50
+ path = "#{api_endpoint.gsub(/new/, ':secret@new')}/review/1"
51
+ assert_requested :get, path, times: 1
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ class Request < Rack::Request
6
+ def delegate?
7
+ false
8
+ end
9
+ end
10
+
11
+ describe Castle::Client do
12
+ let(:ip) { '1.2.3.4' }
13
+ let(:cookie_id) { 'abcd' }
14
+ let(:env) do
15
+ Rack::MockRequest.env_for('/',
16
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
17
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh")
18
+ end
19
+ let(:request) { Request.new(env) }
20
+ let(:client) { described_class.new(request, nil) }
21
+ let(:review_id) { '12356789' }
22
+
23
+ describe 'parses the request' do
24
+ let(:api_data) { [cookie_id, ip, "{\"X-Forwarded-For\":\"#{ip}\"}"] }
25
+
26
+ before do
27
+ allow(Castle::API).to receive(:new).with(*api_data).and_call_original
28
+ end
29
+
30
+ it do
31
+ client.authenticate(name: '$login.succeeded', user_id: '1234')
32
+ expect(Castle::API).to have_received(:new).with(*api_data)
33
+ end
34
+ end
35
+
36
+ it 'identifies' do
37
+ client.identify(user_id: '1234', traits: { name: 'Jo' })
38
+ assert_requested :post, 'https://:secret@api.castle.io/v1/identify',
39
+ times: 1,
40
+ body: { user_id: '1234', traits: { name: 'Jo' } }
41
+ end
42
+
43
+ it 'authenticates' do
44
+ client.authenticate(name: '$login.succeeded', user_id: '1234')
45
+ assert_requested :post, 'https://:secret@api.castle.io/v1/authenticate',
46
+ times: 1,
47
+ body: { name: '$login.succeeded', user_id: '1234' }
48
+ end
49
+
50
+ it 'tracks' do
51
+ client.track(name: '$login.succeeded', user_id: '1234')
52
+ assert_requested :post, 'https://:secret@api.castle.io/v1/track',
53
+ times: 1,
54
+ body: { name: '$login.succeeded', user_id: '1234' }
55
+ end
56
+
57
+ it 'fetches review' do
58
+ client.fetch_review(review_id)
59
+
60
+ assert_requested :get,
61
+ "https://:secret@api.castle.io/v1/reviews/#{review_id}",
62
+ times: 1
63
+ end
64
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::Configuration do
6
+ subject(:config) do
7
+ described_class.new
8
+ end
9
+
10
+ describe 'api_endpoint' do
11
+ context 'env' do
12
+ let(:value) { 50.0 }
13
+
14
+ before do
15
+ allow(ENV).to receive(:fetch).with(
16
+ 'CASTLE_API_ENDPOINT', 'https://api.castle.io/v1'
17
+ ).and_return('https://new.herokuapp.com')
18
+ allow(ENV).to receive(:fetch).with(
19
+ 'CASTLE_API_SECRET', ''
20
+ ).and_call_original
21
+ end
22
+
23
+ it do
24
+ expect(config.api_endpoint).to be_eql(URI('https://new.herokuapp.com'))
25
+ end
26
+ end
27
+
28
+ it do
29
+ expect(config.api_endpoint).to be_eql(URI('https://api.castle.io/v1'))
30
+ end
31
+ end
32
+
33
+ describe 'api_secret' do
34
+ context 'env' do
35
+ before do
36
+ allow(ENV).to receive(:fetch).with(
37
+ 'CASTLE_API_ENDPOINT', 'https://api.castle.io/v1'
38
+ ).and_call_original
39
+ allow(ENV).to receive(:fetch).with(
40
+ 'CASTLE_API_SECRET', ''
41
+ ).and_return('secret_key')
42
+ end
43
+
44
+ it do
45
+ expect(config.api_secret).to be_eql('secret_key')
46
+ end
47
+ end
48
+
49
+ context 'setter' do
50
+ let(:value) { 'new_secret' }
51
+
52
+ before do
53
+ config.api_secret = value
54
+ end
55
+ it do
56
+ expect(config.api_secret).to be_eql(value)
57
+ end
58
+ end
59
+
60
+ it do
61
+ expect(config.api_secret).to be_eql('')
62
+ end
63
+ end
64
+
65
+ describe 'request_timeout' do
66
+ it do
67
+ expect(config.request_timeout).to be_eql(30.0)
68
+ end
69
+
70
+ context 'setter' do
71
+ let(:value) { 50.0 }
72
+
73
+ before do
74
+ config.request_timeout = value
75
+ end
76
+ it do
77
+ expect(config.request_timeout).to be_eql(value)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'source_header' do
83
+ it do
84
+ expect(config.source_header).to be_eql(nil)
85
+ end
86
+
87
+ context 'setter' do
88
+ let(:value) { 'header' }
89
+
90
+ before do
91
+ config.source_header = value
92
+ end
93
+ it do
94
+ expect(config.source_header).to be_eql(value)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::Extractors::ClientId do
6
+ subject(:extractor) { described_class.new(request) }
7
+
8
+ let(:client_id) { 'abcd' }
9
+ let(:request) { Rack::Request.new(env) }
10
+ let(:env) do
11
+ Rack::MockRequest.env_for('/', headers)
12
+ end
13
+
14
+ context 'with client_id' do
15
+ let(:headers) do
16
+ {
17
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
18
+ 'HTTP_COOKIE' => "__cid=#{client_id};other=efgh"
19
+ }
20
+ end
21
+
22
+ it do
23
+ expect(extractor.call(nil, '__cid')).to eql(client_id)
24
+ end
25
+ end
26
+
27
+ context 'with X-Castle-Client-Id header' do
28
+ let(:headers) do
29
+ {
30
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
31
+ 'HTTP_X_CASTLE_CLIENT_ID' => client_id
32
+ }
33
+ end
34
+
35
+ it 'appends the client_id' do
36
+ expect(extractor.call(nil, '__cid')).to eql(client_id)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::Extractors::Headers do
6
+ subject(:extractor) { described_class.new(request) }
7
+
8
+ let(:client_id) { 'abcd' }
9
+ let(:env) do
10
+ Rack::MockRequest.env_for('/',
11
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4',
12
+ 'HTTP_TEST' => '2',
13
+ 'TEST' => '1',
14
+ 'HTTP_COOKIE' => "__cid=#{client_id};other=efgh")
15
+ end
16
+ let(:request) { Rack::Request.new(env) }
17
+
18
+ describe 'header should extract http headers but skip cookies related' do
19
+ it do
20
+ expect(extractor.call).to eql(
21
+ '{"X-Forwarded-For":"1.2.3.4","Test":"2"}'
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::Extractors::IP do
6
+ subject(:extractor) { described_class.new(request) }
7
+
8
+ let(:request) { Rack::Request.new(env) }
9
+
10
+ describe 'ip' do
11
+ let(:env) do
12
+ Rack::MockRequest.env_for('/',
13
+ 'HTTP_X_FORWARDED_FOR' => '1.2.3.4')
14
+ end
15
+
16
+ it do
17
+ expect(extractor.call).to eql('1.2.3.4')
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Castle::Headers do
6
+ subject do
7
+ described_class.new
8
+ end
9
+
10
+ before do
11
+ allow(Castle.config).to receive(:source_header).and_return('web')
12
+ stub_const('Castle::VERSION', '2.2.0')
13
+ end
14
+
15
+ let(:ip) { '1.1.1.1' }
16
+ let(:castle_headers) { 'headers' }
17
+ let(:client_id) { 'some_id' }
18
+
19
+ let(:prepared_headers) do
20
+ subject.prepare(client_id, ip, castle_headers)
21
+ end
22
+
23
+ shared_examples 'for_header' do |header, value|
24
+ let(:header) { header }
25
+
26
+ it "has header #{header}" do
27
+ expect(prepared_headers[header]).to eql(value)
28
+ end
29
+ end
30
+ it_behaves_like 'for_header', 'Content-Type', 'application/json'
31
+ it_behaves_like 'for_header', 'X-Castle-Client-Id', 'some_id'
32
+ it_behaves_like 'for_header', 'X-Castle-Ip', '1.1.1.1'
33
+ it_behaves_like 'for_header', 'X-Castle-Headers', 'headers'
34
+ it_behaves_like 'for_header', 'X-Castle-Source', 'web'
35
+ it_behaves_like 'for_header',
36
+ 'User-Agent',
37
+ 'Castle/v1 RubyBindings/2.2.0'
38
+
39
+ describe 'X-Castle-Client-User-Agent' do
40
+ let(:header) { 'X-Castle-Client-User-Agent' }
41
+ let(:prepared_header_parsed) { JSON.parse(prepared_headers[header]) }
42
+
43
+ before do
44
+ allow(Castle::System).to receive(:uname).and_return('name')
45
+ end
46
+
47
+ it { expect(prepared_header_parsed).to include('lang') }
48
+ it { expect(prepared_header_parsed).to include('lang_version') }
49
+ it { expect(prepared_header_parsed).to include('platform') }
50
+ it { expect(prepared_header_parsed).to include('publisher') }
51
+ it { expect(prepared_header_parsed['bindings_version']).to eql('2.2.0') }
52
+ it { expect(prepared_header_parsed['uname']).to eql('name') }
53
+ end
54
+
55
+ context 'missing' do
56
+ shared_examples 'for_missing_header' do |header|
57
+ let(:header) { header }
58
+
59
+ it "it missing header #{header}" do
60
+ expect(prepared_headers.key?(header)).to be_falsey
61
+ end
62
+ end
63
+
64
+ context 'ip' do
65
+ let(:ip) { nil }
66
+
67
+ it_behaves_like 'for_missing_header', 'X-Castle-Ip'
68
+ end
69
+
70
+ context 'client_id' do
71
+ let(:client_id) { nil }
72
+
73
+ it_behaves_like 'for_missing_header', 'X-Castle-Cookie-Id'
74
+ end
75
+
76
+ context 'castle headers' do
77
+ let(:castle_headers) { nil }
78
+
79
+ it_behaves_like 'for_missing_header', 'X-Castle-Headers'
80
+ end
81
+ end
82
+ end