castle-rb 4.1.0 → 6.0.0

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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +158 -43
  3. data/lib/castle.rb +46 -21
  4. data/lib/castle/api.rb +24 -12
  5. data/lib/castle/api/approve_device.rb +25 -0
  6. data/lib/castle/api/authenticate.rb +34 -0
  7. data/lib/castle/api/end_impersonation.rb +29 -0
  8. data/lib/castle/api/get_device.rb +25 -0
  9. data/lib/castle/api/get_devices_for_user.rb +25 -0
  10. data/lib/castle/api/identify.rb +26 -0
  11. data/lib/castle/api/report_device.rb +25 -0
  12. data/lib/castle/api/review.rb +24 -0
  13. data/lib/castle/api/start_impersonation.rb +29 -0
  14. data/lib/castle/api/track.rb +26 -0
  15. data/lib/castle/client.rb +52 -45
  16. data/lib/castle/{extractors/client_id.rb → client_id/extract.rb} +2 -2
  17. data/lib/castle/commands/approve_device.rb +21 -0
  18. data/lib/castle/commands/authenticate.rb +13 -13
  19. data/lib/castle/commands/end_impersonation.rb +25 -0
  20. data/lib/castle/commands/get_device.rb +21 -0
  21. data/lib/castle/commands/get_devices_for_user.rb +21 -0
  22. data/lib/castle/commands/identify.rb +12 -13
  23. data/lib/castle/commands/report_device.rb +21 -0
  24. data/lib/castle/commands/review.rb +6 -3
  25. data/lib/castle/commands/start_impersonation.rb +25 -0
  26. data/lib/castle/commands/track.rb +12 -13
  27. data/lib/castle/configuration.rb +45 -28
  28. data/lib/castle/context/{default.rb → get_default.rb} +5 -6
  29. data/lib/castle/context/{merger.rb → merge.rb} +3 -3
  30. data/lib/castle/context/prepare.rb +18 -0
  31. data/lib/castle/context/{sanitizer.rb → sanitize.rb} +1 -1
  32. data/lib/castle/core/get_connection.rb +25 -0
  33. data/lib/castle/{api/response.rb → core/process_response.rb} +4 -2
  34. data/lib/castle/core/process_webhook.rb +20 -0
  35. data/lib/castle/core/send_request.rb +50 -0
  36. data/lib/castle/errors.rb +2 -0
  37. data/lib/castle/events.rb +1 -1
  38. data/lib/castle/failover/prepare_response.rb +23 -0
  39. data/lib/castle/failover/strategy.rb +20 -0
  40. data/lib/castle/headers/extract.rb +47 -0
  41. data/lib/castle/headers/filter.rb +37 -0
  42. data/lib/castle/headers/format.rb +24 -0
  43. data/lib/castle/ip/extract.rb +83 -0
  44. data/lib/castle/logger.rb +19 -0
  45. data/lib/castle/payload/prepare.rb +27 -0
  46. data/lib/castle/secure_mode.rb +6 -2
  47. data/lib/castle/session.rb +18 -0
  48. data/lib/castle/singleton_configuration.rb +9 -0
  49. data/lib/castle/utils/clean_invalid_chars.rb +24 -0
  50. data/lib/castle/utils/clone.rb +15 -0
  51. data/lib/castle/utils/deep_symbolize_keys.rb +45 -0
  52. data/lib/castle/utils/get_timestamp.rb +15 -0
  53. data/lib/castle/utils/{merger.rb → merge.rb} +3 -3
  54. data/lib/castle/utils/secure_compare.rb +22 -0
  55. data/lib/castle/validators/not_supported.rb +1 -0
  56. data/lib/castle/validators/present.rb +1 -0
  57. data/lib/castle/verdict.rb +13 -0
  58. data/lib/castle/version.rb +1 -1
  59. data/lib/castle/webhooks/verify.rb +43 -0
  60. data/spec/integration/rails/rails_spec.rb +33 -7
  61. data/spec/integration/rails/support/application.rb +3 -1
  62. data/spec/integration/rails/support/home_controller.rb +47 -5
  63. data/spec/lib/castle/api/approve_device_spec.rb +21 -0
  64. data/spec/lib/castle/api/authenticate_spec.rb +140 -0
  65. data/spec/lib/castle/api/end_impersonation_spec.rb +59 -0
  66. data/spec/lib/castle/api/get_device_spec.rb +19 -0
  67. data/spec/lib/castle/api/get_devices_for_user_spec.rb +19 -0
  68. data/spec/lib/castle/api/identify_spec.rb +68 -0
  69. data/spec/lib/castle/api/report_device_spec.rb +21 -0
  70. data/spec/lib/castle/{review_spec.rb → api/review_spec.rb} +3 -3
  71. data/spec/lib/castle/api/start_impersonation_spec.rb +59 -0
  72. data/spec/lib/castle/api/track_spec.rb +68 -0
  73. data/spec/lib/castle/api_spec.rb +16 -1
  74. data/spec/lib/castle/{extractors/client_id_spec.rb → client_id/extract_spec.rb} +2 -2
  75. data/spec/lib/castle/client_spec.rb +41 -23
  76. data/spec/lib/castle/commands/approve_device_spec.rb +24 -0
  77. data/spec/lib/castle/commands/authenticate_spec.rb +7 -16
  78. data/spec/lib/castle/commands/end_impersonation_spec.rb +82 -0
  79. data/spec/lib/castle/commands/get_device_spec.rb +24 -0
  80. data/spec/lib/castle/commands/get_devices_for_user_spec.rb +24 -0
  81. data/spec/lib/castle/commands/identify_spec.rb +5 -16
  82. data/spec/lib/castle/commands/report_device_spec.rb +24 -0
  83. data/spec/lib/castle/commands/review_spec.rb +1 -1
  84. data/spec/lib/castle/commands/{impersonate_spec.rb → start_impersonation_spec.rb} +9 -34
  85. data/spec/lib/castle/commands/track_spec.rb +5 -16
  86. data/spec/lib/castle/configuration_spec.rb +9 -138
  87. data/spec/lib/castle/context/{default_spec.rb → get_default_spec.rb} +1 -2
  88. data/spec/lib/castle/context/{merger_spec.rb → merge_spec.rb} +1 -1
  89. data/spec/lib/castle/context/prepare_spec.rb +44 -0
  90. data/spec/lib/castle/context/{sanitizer_spec.rb → sanitize_spec.rb} +1 -1
  91. data/spec/lib/castle/core/get_connection_spec.rb +59 -0
  92. data/spec/lib/castle/{api/response_spec.rb → core/process_response_spec.rb} +56 -1
  93. data/spec/lib/castle/core/process_webhook_spec.rb +46 -0
  94. data/spec/lib/castle/core/send_request_spec.rb +102 -0
  95. data/spec/lib/castle/failover/strategy_spec.rb +12 -0
  96. data/spec/lib/castle/{extractors/headers_spec.rb → headers/extract_spec.rb} +18 -18
  97. data/spec/lib/castle/{headers_filter_spec.rb → headers/filter_spec.rb} +6 -5
  98. data/spec/lib/castle/headers/format_spec.rb +25 -0
  99. data/spec/lib/castle/{extractors/ip_spec.rb → ip/extract_spec.rb} +35 -7
  100. data/spec/lib/castle/logger_spec.rb +42 -0
  101. data/spec/lib/castle/payload/prepare_spec.rb +54 -0
  102. data/spec/lib/castle/session_spec.rb +88 -0
  103. data/spec/lib/castle/singleton_configuration_spec.rb +18 -0
  104. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +69 -0
  105. data/spec/lib/castle/utils/{cloner_spec.rb → clone_spec.rb} +3 -3
  106. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +50 -0
  107. data/spec/lib/castle/utils/{timestamp_spec.rb → get_timestamp_spec.rb} +1 -1
  108. data/spec/lib/castle/utils/{merger_spec.rb → merge_spec.rb} +3 -3
  109. data/spec/lib/castle/verdict_spec.rb +9 -0
  110. data/spec/lib/castle/webhooks/verify_spec.rb +69 -0
  111. data/spec/spec_helper.rb +2 -0
  112. data/spec/support/shared_examples/configuration.rb +129 -0
  113. metadata +133 -56
  114. data/lib/castle/api/request.rb +0 -42
  115. data/lib/castle/api/session.rb +0 -39
  116. data/lib/castle/commands/impersonate.rb +0 -26
  117. data/lib/castle/extractors/headers.rb +0 -45
  118. data/lib/castle/extractors/ip.rb +0 -68
  119. data/lib/castle/failover_auth_response.rb +0 -21
  120. data/lib/castle/headers_filter.rb +0 -35
  121. data/lib/castle/headers_formatter.rb +0 -22
  122. data/lib/castle/review.rb +0 -11
  123. data/lib/castle/utils.rb +0 -55
  124. data/lib/castle/utils/cloner.rb +0 -11
  125. data/lib/castle/utils/timestamp.rb +0 -12
  126. data/spec/lib/castle/api/request_spec.rb +0 -72
  127. data/spec/lib/castle/headers_formatter_spec.rb +0 -25
  128. data/spec/lib/castle/utils_spec.rb +0 -156
@@ -2,146 +2,17 @@
2
2
 
3
3
  describe Castle::Configuration do
4
4
  subject(:config) do
5
- described_class.instance
5
+ described_class.new
6
6
  end
7
7
 
8
- describe 'host' do
9
- context 'with default' do
10
- it { expect(config.host).to be_eql('api.castle.io') }
11
- end
8
+ it_behaves_like 'configuration_host'
9
+ it_behaves_like 'configuration_request_timeout'
10
+ it_behaves_like 'configuration_allowlisted'
11
+ it_behaves_like 'configuration_denylisted'
12
+ it_behaves_like 'configuration_failover_strategy'
13
+ it_behaves_like 'configuration_api_secret'
12
14
 
13
- context 'with setter' do
14
- before { config.host = 'api.castle.dev' }
15
-
16
- it { expect(config.host).to be_eql('api.castle.dev') }
17
- end
18
- end
19
-
20
- describe 'post' do
21
- context 'with default' do
22
- it { expect(config.port).to be_eql(443) }
23
- end
24
-
25
- context 'with setter' do
26
- before { config.port = 3001 }
27
-
28
- it { expect(config.port).to be_eql(3001) }
29
- end
30
- end
31
-
32
- describe 'api_secret' do
33
- context 'with env' do
34
- let(:secret_key_env) { 'secret_key_env' }
35
- let(:secret_key) { 'secret_key' }
36
-
37
- before do
38
- allow(ENV).to receive(:fetch).with(
39
- 'CASTLE_API_SECRET', ''
40
- ).and_return(secret_key_env)
41
- config.reset
42
- end
43
-
44
- it do
45
- expect(config.api_secret).to be_eql(secret_key_env)
46
- end
47
-
48
- context 'when key is overwritten' do
49
- before { config.api_secret = secret_key }
50
-
51
- it do
52
- expect(config.api_secret).to be_eql(secret_key)
53
- end
54
- end
55
- end
56
-
57
- context 'with setter' do
58
- let(:value) { 'new_secret' }
59
-
60
- before do
61
- config.api_secret = value
62
- end
63
-
64
- it do
65
- expect(config.api_secret).to be_eql(value)
66
- end
67
- end
68
-
69
- it do
70
- expect(config.api_secret).to be_eql('secret')
71
- end
72
- end
73
-
74
- describe 'request_timeout' do
75
- it do
76
- expect(config.request_timeout).to be_eql(500)
77
- end
78
-
79
- context 'with setter' do
80
- let(:value) { 50.0 }
81
-
82
- before do
83
- config.request_timeout = value
84
- end
85
-
86
- it do
87
- expect(config.request_timeout).to be_eql(value)
88
- end
89
- end
90
- end
91
-
92
- describe 'whitelisted' do
93
- it do
94
- expect(config.whitelisted.size).to be_eql(0)
95
- end
96
-
97
- context 'with setter' do
98
- before do
99
- config.whitelisted = ['header']
100
- end
101
-
102
- it do
103
- expect(config.whitelisted).to be_eql(['Header'])
104
- end
105
- end
106
- end
107
-
108
- describe 'blacklisted' do
109
- it do
110
- expect(config.blacklisted.size).to be_eql(0)
111
- end
112
-
113
- context 'with setter' do
114
- before do
115
- config.blacklisted = ['header']
116
- end
117
-
118
- it do
119
- expect(config.blacklisted).to be_eql(['Header'])
120
- end
121
- end
122
- end
123
-
124
- describe 'failover_strategy' do
125
- it do
126
- expect(config.failover_strategy).to be_eql(:allow)
127
- end
128
-
129
- context 'with setter' do
130
- before do
131
- config.failover_strategy = :deny
132
- end
133
-
134
- it do
135
- expect(config.failover_strategy).to be_eql(:deny)
136
- end
137
- end
138
-
139
- context 'when broken' do
140
- it do
141
- expect do
142
- config.failover_strategy = :unicorn
143
- end.to raise_error(Castle::ConfigurationError)
144
- end
145
- end
15
+ it do
16
+ expect(config.api_secret).to be_eql('')
146
17
  end
147
18
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Context::Default do
3
+ describe Castle::Context::GetDefault do
4
4
  subject { described_class.new(request, nil) }
5
5
 
6
6
  let(:ip) { '1.2.3.4' }
@@ -31,7 +31,6 @@ describe Castle::Context::Default do
31
31
  end
32
32
 
33
33
  it { expect(default_context[:active]).to be_eql(true) }
34
- it { expect(default_context[:origin]).to be_eql('web') }
35
34
  it { expect(default_context[:headers]).to be_eql(result_headers) }
36
35
  it { expect(default_context[:ip]).to be_eql(ip) }
37
36
  it { expect(default_context[:client_id]).to be_eql(client_id) }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Context::Merger do
3
+ describe Castle::Context::Merge do
4
4
  let(:first) { { test: { test1: { c: '4' } } } }
5
5
 
6
6
  describe '#call' do
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Context::Prepare do
4
+ let(:ip) { '1.2.3.4' }
5
+ let(:cookie_id) { 'abcd' }
6
+ let(:ua) { 'Chrome' }
7
+ let(:env) do
8
+ Rack::MockRequest.env_for(
9
+ '/',
10
+ 'HTTP_USER_AGENT' => ua,
11
+ 'HTTP_X_FORWARDED_FOR' => ip,
12
+ 'HTTP_COOKIE' => "__cid=#{cookie_id};other=efgh"
13
+ )
14
+ end
15
+ let(:request) { Rack::Request.new(env) }
16
+ let(:context) do
17
+ {
18
+ client_id: 'abcd',
19
+ active: true,
20
+ user_agent: ua,
21
+ headers: headers,
22
+ ip: ip,
23
+ library: { name: 'castle-rb', version: '6.0.0' }
24
+ }
25
+ end
26
+
27
+ let(:headers) do
28
+ {
29
+ 'Content-Length': '0', 'User-Agent': ua, 'X-Forwarded-For': ip.to_s, 'Cookie': true
30
+ }
31
+ end
32
+
33
+ before do
34
+ stub_const('Castle::VERSION', '6.0.0')
35
+ end
36
+
37
+ describe '#call' do
38
+ subject(:generated) { described_class.call(request) }
39
+
40
+ context 'when active true' do
41
+ it { is_expected.to eql(context) }
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::Context::Sanitizer do
3
+ describe Castle::Context::Sanitize do
4
4
  let(:paylod) { { test: 'test' } }
5
5
 
6
6
  describe '#call' do
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Core::GetConnection do
4
+ describe '.call' do
5
+ subject(:class_call) { described_class.call }
6
+
7
+ context 'when ssl false' do
8
+ let(:localhost) { 'localhost' }
9
+ let(:port) { 3002 }
10
+ let(:api_url) { '/test' }
11
+
12
+ before do
13
+ Castle.config.base_url = 'http://localhost:3002'
14
+
15
+ allow(Net::HTTP)
16
+ .to receive(:new)
17
+ .with(localhost, port)
18
+ .and_call_original
19
+ end
20
+
21
+ it do
22
+ class_call
23
+
24
+ expect(Net::HTTP)
25
+ .to have_received(:new)
26
+ .with(localhost, port)
27
+ end
28
+
29
+ it do
30
+ expect(class_call).to be_an_instance_of(Net::HTTP)
31
+ end
32
+ end
33
+
34
+ context 'when ssl true' do
35
+ let(:localhost) { 'localhost' }
36
+ let(:port) { 443 }
37
+
38
+ before do
39
+ Castle.config.base_url = 'https://localhost'
40
+ end
41
+
42
+ context 'with block' do
43
+ let(:api_url) { '/test' }
44
+ let(:request) { Net::HTTP::Get.new(api_url) }
45
+
46
+ before do
47
+ allow(Net::HTTP)
48
+ .to receive(:new)
49
+ .with(localhost, port)
50
+ .and_call_original
51
+ end
52
+
53
+ it do
54
+ expect(class_call).to be_an_instance_of(Net::HTTP)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Castle::API::Response do
3
+ describe Castle::Core::ProcessResponse do
4
4
  describe '#call' do
5
5
  subject(:call) { described_class.call(response) }
6
6
 
@@ -10,6 +10,61 @@ describe Castle::API::Response do
10
10
  it { expect(call).to eql(user: 1) }
11
11
  end
12
12
 
13
+ describe 'authenticate' do
14
+ context 'when allow without any additional props' do
15
+ let(:response) { OpenStruct.new(body: '{"action":"allow","user_id":"12345"}', code: 200) }
16
+
17
+ it { expect(call).to eql({ action: 'allow', user_id: '12345' }) }
18
+ end
19
+
20
+ context 'when allow with additional props' do
21
+ let(:response) do
22
+ OpenStruct.new(body: '{"action":"allow","user_id":"12345","internal":{}}', code: 200)
23
+ end
24
+
25
+ it { expect(call).to eql({ action: 'allow', user_id: '12345', internal: {} }) }
26
+ end
27
+
28
+ context 'when deny without risk policy' do
29
+ let(:response) do
30
+ OpenStruct.new(body: '{"action":"deny","user_id":"1","device_token":"abc"}', code: 200)
31
+ end
32
+
33
+ it { expect(call).to eql({ action: 'deny', user_id: '1', device_token: 'abc' }) }
34
+ end
35
+
36
+ context 'when deny with risk policy' do
37
+ let(:body) do
38
+ '{"action":"deny","user_id":"1","device_token":"abc",
39
+ "risk_policy":{"id":"123","revision_id":"abc","name":"def","type":"bot"}}'
40
+ end
41
+ let(:response) do
42
+ OpenStruct.new(
43
+ {
44
+ body: body,
45
+ code: 200
46
+ }
47
+ )
48
+ end
49
+
50
+ let(:result) do
51
+ {
52
+ action: 'deny',
53
+ user_id: '1',
54
+ device_token: 'abc',
55
+ risk_policy: {
56
+ id: '123',
57
+ revision_id: 'abc',
58
+ name: 'def',
59
+ type: 'bot'
60
+ }
61
+ }
62
+ end
63
+
64
+ it { expect(call).to eql(result) }
65
+ end
66
+ end
67
+
13
68
  context 'when response empty' do
14
69
  let(:response) { OpenStruct.new(body: '', code: 200) }
15
70
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Core::ProcessWebhook do
4
+ describe '#call' do
5
+ subject(:call) { described_class.call(webhook) }
6
+
7
+ let(:webhook_body) do
8
+ {
9
+ api_version: 'v1',
10
+ app_id: '12345',
11
+ type: '$incident.confirmed',
12
+ created_at: '2020-12-18T12:55:21.779Z',
13
+ data: {
14
+ id: 'test',
15
+ device_token: 'token',
16
+ user_id: '',
17
+ trigger: '$login.succeeded',
18
+ context: {},
19
+ location: {},
20
+ user_agent: {}
21
+ },
22
+ user_traits: {},
23
+ properties: {},
24
+ policy: {}
25
+ }.to_json
26
+ end
27
+
28
+ let(:webhook) { OpenStruct.new(body: StringIO.new(webhook_body)) }
29
+
30
+ context 'when success' do
31
+ it { expect(call).to eql(webhook_body) }
32
+ end
33
+
34
+ context 'when webhook empty' do
35
+ let(:webhook) { OpenStruct.new(body: StringIO.new('')) }
36
+
37
+ it { expect { call }.to raise_error(Castle::ApiError, 'Invalid webhook from Castle API') }
38
+ end
39
+
40
+ context 'when webhook nil' do
41
+ let(:webhook) { OpenStruct.new(body: StringIO.new) }
42
+
43
+ it { expect { call }.to raise_error(Castle::ApiError, 'Invalid webhook from Castle API') }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Castle::Core::SendRequest do
4
+ let(:config) { Castle.config }
5
+
6
+ describe '#call' do
7
+ let(:command) { Castle::Commands::Track.build(event: '$login.succeeded') }
8
+ let(:headers) { {} }
9
+ let(:request_build) { {} }
10
+ let(:expected_headers) { { 'Content-Type' => 'application/json' } }
11
+ let(:http) { instance_double('Net::HTTP') }
12
+
13
+ context 'without http arg provided' do
14
+ subject(:call) { described_class.call(command, headers, nil, config) }
15
+
16
+ let(:http) { instance_double('Net::HTTP') }
17
+ let(:command) { Castle::Commands::Track.build(event: '$login.succeeded') }
18
+ let(:headers) { {} }
19
+ let(:request_build) { {} }
20
+ let(:expected_headers) { { 'Content-Type' => 'application/json' } }
21
+
22
+ before do
23
+ allow(Castle::Core::GetConnection).to receive(:call).and_return(http)
24
+ allow(http).to receive(:request)
25
+ allow(described_class).to receive(:build).and_return(request_build)
26
+ call
27
+ end
28
+
29
+ it do
30
+ expect(described_class).to have_received(:build).with(
31
+ command, expected_headers, config
32
+ )
33
+ end
34
+
35
+ it { expect(http).to have_received(:request).with(request_build) }
36
+ end
37
+
38
+ context 'with http arg provided' do
39
+ subject(:call) { described_class.call(command, headers, http, config) }
40
+
41
+ before do
42
+ allow(Castle::Core::GetConnection).to receive(:call)
43
+ allow(http).to receive(:request)
44
+ allow(described_class).to receive(:build).and_return(request_build)
45
+ call
46
+ end
47
+
48
+ it { expect(Castle::Core::GetConnection).not_to have_received(:call) }
49
+
50
+ it do
51
+ expect(described_class).to have_received(:build).with(
52
+ command, expected_headers, config
53
+ )
54
+ end
55
+
56
+ it { expect(http).to have_received(:request).with(request_build) }
57
+ end
58
+ end
59
+
60
+ describe '#build' do
61
+ subject(:build) { described_class.build(command, headers, config) }
62
+
63
+ let(:headers) { { 'SAMPLE-HEADER' => '1' } }
64
+ let(:api_secret) { 'secret' }
65
+
66
+ context 'when get' do
67
+ let(:command) { Castle::Commands::Review.build({ review_id: review_id }) }
68
+ let(:review_id) { SecureRandom.uuid }
69
+
70
+ it { expect(build.body).to be_nil }
71
+ it { expect(build.method).to eql('GET') }
72
+ it { expect(build.path).to eql("/v1/#{command.path}") }
73
+ it { expect(build.to_hash).to have_key('authorization') }
74
+ it { expect(build.to_hash).to have_key('sample-header') }
75
+ it { expect(build.to_hash['sample-header']).to eql(['1']) }
76
+ end
77
+
78
+ context 'when post' do
79
+ let(:time) { Time.now.utc.iso8601(3) }
80
+ let(:command) do
81
+ Castle::Commands::Track.build(event: '$login.succeeded', name: "\xC4")
82
+ end
83
+ let(:expected_body) do
84
+ {
85
+ event: '$login.succeeded',
86
+ name: '�',
87
+ context: {},
88
+ sent_at: time
89
+ }
90
+ end
91
+
92
+ before { allow(Castle::Utils::GetTimestamp).to receive(:call).and_return(time) }
93
+
94
+ it { expect(build.body).to be_eql(expected_body.to_json) }
95
+ it { expect(build.method).to eql('POST') }
96
+ it { expect(build.path).to eql("/v1/#{command.path}") }
97
+ it { expect(build.to_hash).to have_key('authorization') }
98
+ it { expect(build.to_hash).to have_key('sample-header') }
99
+ it { expect(build.to_hash['sample-header']).to eql(['1']) }
100
+ end
101
+ end
102
+ end