sift 1.1.6.2 → 4.5.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 (45) hide show
  1. checksums.yaml +5 -13
  2. data/.circleci/config.yml +105 -0
  3. data/.github/pull_request_template.md +12 -0
  4. data/.github/workflows/publishing_sift_ruby.yml +38 -0
  5. data/.gitignore +1 -0
  6. data/.jenkins/Jenkinsfile +103 -0
  7. data/HISTORY +104 -0
  8. data/README.md +351 -0
  9. data/examples/psp_merchant_management_apis.rb +105 -0
  10. data/examples/validation_apis.rb +47 -0
  11. data/lib/sift/client/decision/apply_to.rb +129 -0
  12. data/lib/sift/client/decision.rb +66 -0
  13. data/lib/sift/client.rb +845 -112
  14. data/lib/sift/error.rb +13 -0
  15. data/lib/sift/router.rb +41 -0
  16. data/lib/sift/utils/hash_getter.rb +15 -0
  17. data/lib/sift/validate/decision.rb +65 -0
  18. data/lib/sift/validate/primitive.rb +43 -0
  19. data/lib/sift/version.rb +2 -2
  20. data/lib/sift.rb +85 -11
  21. data/sift.gemspec +5 -3
  22. data/spec/fixtures/fake_responses.rb +16 -0
  23. data/spec/spec_helper.rb +1 -1
  24. data/spec/unit/client/decision/apply_to_spec.rb +262 -0
  25. data/spec/unit/client/decision_spec.rb +83 -0
  26. data/spec/unit/client_203_spec.rb +193 -0
  27. data/spec/unit/client_205_spec.rb +117 -0
  28. data/spec/unit/client_label_spec.rb +68 -11
  29. data/spec/unit/client_psp_merchant_spec.rb +133 -0
  30. data/spec/unit/client_spec.rb +556 -79
  31. data/spec/unit/client_validationapi_spec.rb +91 -0
  32. data/spec/unit/router_spec.rb +37 -0
  33. data/spec/unit/validate/decision_spec.rb +85 -0
  34. data/spec/unit/validate/primitive_spec.rb +73 -0
  35. data/test_integration_app/decisions_api/test_decisions_api.rb +31 -0
  36. data/test_integration_app/events_api/test_events_api.rb +843 -0
  37. data/test_integration_app/globals.rb +2 -0
  38. data/test_integration_app/main.rb +67 -0
  39. data/test_integration_app/psp_merchants_api/test_psp_merchant_api.rb +44 -0
  40. data/test_integration_app/score_api/test_score_api.rb +11 -0
  41. data/test_integration_app/verification_api/test_verification_api.rb +32 -0
  42. metadata +85 -28
  43. data/.travis.yml +0 -13
  44. data/README.rdoc +0 -85
  45. data/spec/unit/sift_spec.rb +0 -6
data/lib/sift/error.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Sift
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ApiError < Error
6
+ attr_reader :message, :response
7
+
8
+ def initialize(message, response)
9
+ @message = message
10
+ @response = response
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "./version"
2
+ require_relative "./client"
3
+
4
+ module Sift
5
+ class Router
6
+ include HTTParty
7
+
8
+ class << self
9
+ def get(path, options = {})
10
+ serialize_body(options)
11
+ add_default_headers(options)
12
+ wrap_response(super(path, options))
13
+ end
14
+
15
+ def post(path, options = {})
16
+ serialize_body(options)
17
+ add_default_headers(options)
18
+ wrap_response(super(path, options))
19
+ end
20
+
21
+ def serialize_body(options)
22
+ options[:body] = MultiJson.dump(options[:body]) if options[:body]
23
+ end
24
+
25
+ def add_default_headers(options)
26
+ options[:headers] = {
27
+ "User-Agent" => Sift::Client.user_agent
28
+ }.merge(options[:headers] || {})
29
+ end
30
+
31
+ def wrap_response(response)
32
+ Response.new(
33
+ response.body,
34
+ response.code,
35
+ response.response
36
+ )
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Sift
2
+ module Utils
3
+ class HashGetter
4
+ attr_reader :hash
5
+
6
+ def initialize(hash)
7
+ @hash = hash
8
+ end
9
+
10
+ def get(value)
11
+ hash[value.to_sym] || hash[value.to_s]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "primitive"
2
+
3
+ module Sift
4
+ module Validate
5
+ class Decision
6
+ attr_reader :configs, :error_messages
7
+
8
+ def initialize(configs = {})
9
+ @configs = configs
10
+ end
11
+
12
+ def valid_order?
13
+ run do
14
+ validate_key(:non_empty_string, :user_id, :order_id)
15
+ end
16
+ end
17
+
18
+ def valid_session?
19
+ run do
20
+ validate_key(:non_empty_string, :user_id, :session_id)
21
+ end
22
+ end
23
+
24
+ def valid_content?
25
+ run do
26
+ validate_key(:non_empty_string, :user_id, :content_id)
27
+ end
28
+ end
29
+
30
+ def valid_user?
31
+ run do
32
+ validate_key(:non_empty_string, :user_id)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def run
39
+ clear_errors!
40
+ yield
41
+ error_messages.empty?
42
+ end
43
+
44
+ def validate_primitive
45
+ Validate::Primitive
46
+ end
47
+
48
+ def validate_key(type, *keys)
49
+ keys.each do |key|
50
+ if error_message = validate_primitive.public_send(type, get(key))
51
+ error_messages << "#{key} #{error_message}"
52
+ end
53
+ end
54
+ end
55
+
56
+ def clear_errors!
57
+ @error_messages = []
58
+ end
59
+
60
+ def get(key)
61
+ configs[key] || configs[key.to_s]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,43 @@
1
+ module Sift
2
+ module Validate
3
+ class Primitive
4
+ ERROR_MESSAGES = {
5
+ non_empty_string: "must be a non-empty string",
6
+ numeric: "must be a number",
7
+ string_or_number: "must be a string or a number",
8
+ }
9
+
10
+ class << self
11
+ def non_empty_string(value)
12
+ if !value.is_a?(String)
13
+ "#{ERROR_MESSAGES[:non_empty_string]}, got #{value.class}"
14
+ elsif value.empty?
15
+ empty_string_message(:non_empty_string)
16
+ end
17
+ end
18
+
19
+ def numeric(value)
20
+ if !value.is_a?(Numeric)
21
+ ERROR_MESSAGES[:numeric]
22
+ end
23
+ end
24
+
25
+ def string_or_number(value)
26
+ if (value.is_a?(String) && value.empty?)
27
+ return empty_string_message(:string_or_number)
28
+ end
29
+
30
+ if value.nil? || !(value.is_a?(String) || value.is_a?(Numeric))
31
+ "#{ERROR_MESSAGES[:string_or_number]}, got #{value.class}"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def empty_string_message(message)
38
+ "#{ERROR_MESSAGES[message]}, got an empty string"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/sift/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Sift
2
- VERSION = "1.1.6.2"
3
- API_VERSION = "203"
2
+ VERSION = "4.5.0"
3
+ API_VERSION = "205"
4
4
  end
data/lib/sift.rb CHANGED
@@ -1,23 +1,98 @@
1
- require "sift/version"
2
- require "sift/client"
1
+ require_relative './sift/version'
2
+ require_relative './sift/client'
3
+ require 'erb'
3
4
 
4
5
  module Sift
5
6
 
6
- # Returns the path for the current API version
7
- def self.current_rest_api_path
8
- "/v#{API_VERSION}/events"
7
+ # Returns the path for the specified API version
8
+ def self.rest_api_path(version=API_VERSION)
9
+ "/v#{version}/events"
9
10
  end
10
11
 
11
- def self.current_users_label_api_path(user_id)
12
- # This API version is a minor version ahead of the /events API
13
- "/v#{API_VERSION}/users/#{URI.encode(user_id)}/labels"
12
+ # Returns the path for the specified API version
13
+ def self.verification_api_send_path(version=API_VERSION)
14
+ "/v#{version}/verification/send"
14
15
  end
15
-
16
- # Adding module scoped public API key
16
+
17
+ # Returns the path for the specified API version
18
+ def self.verification_api_resend_path(version=API_VERSION)
19
+ "/v#{version}/verification/resend"
20
+ end
21
+
22
+ # Returns the path for the specified API version
23
+ def self.verification_api_check_path(version=API_VERSION)
24
+ "/v#{version}/verification/check"
25
+ end
26
+
27
+ # Returns the Score API path for the specified user ID and API version
28
+ def self.score_api_path(user_id, version=API_VERSION)
29
+ "/v#{version}/score/#{ERB::Util.url_encode(user_id)}/"
30
+ end
31
+
32
+ # Returns the User Score API path for the specified user ID and API version
33
+ def self.user_score_api_path(user_id, version=API_VERSION)
34
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/score"
35
+ end
36
+
37
+ # Returns the users API path for the specified user ID and API version
38
+ def self.users_label_api_path(user_id, version=API_VERSION)
39
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/labels"
40
+ end
41
+
42
+ # Returns the path for the Workflow Status API
43
+ def self.workflow_status_path(account_id, run_id)
44
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
45
+ "/workflows/runs/#{ERB::Util.url_encode(run_id)}"
46
+ end
47
+
48
+ # Returns the path for User Decisions API
49
+ def self.user_decisions_api_path(account_id, user_id)
50
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
51
+ "/users/#{ERB::Util.url_encode(user_id)}/decisions"
52
+ end
53
+
54
+ # Returns the path for Orders Decisions API
55
+ def self.order_decisions_api_path(account_id, order_id)
56
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
57
+ "/orders/#{ERB::Util.url_encode(order_id)}/decisions"
58
+ end
59
+
60
+ # Returns the path for Session Decisions API
61
+ def self.session_decisions_api_path(account_id, user_id, session_id)
62
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
63
+ "/users/#{ERB::Util.url_encode(user_id)}" \
64
+ "/sessions/#{ERB::Util.url_encode(session_id)}/decisions"
65
+ end
66
+
67
+ # Returns the path for Content Decisions API
68
+ def self.content_decisions_api_path(account_id, user_id, content_id)
69
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
70
+ "/users/#{ERB::Util.url_encode(user_id)}" \
71
+ "/content/#{ERB::Util.url_encode(content_id)}/decisions"
72
+ end
73
+
74
+ # Returns the path for psp Merchant API
75
+ def self.psp_merchant_api_path(account_id)
76
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
77
+ "/psp_management/merchants"
78
+ end
79
+
80
+ # Returns the path for psp Merchant with id
81
+ def self.psp_merchant_id_api_path(account_id, merchant_id)
82
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
83
+ "/psp_management/merchants/#{ERB::Util.url_encode(merchant_id)}"
84
+ end
85
+
86
+ # Module-scoped public API key
17
87
  class << self
18
88
  attr_accessor :api_key
19
89
  end
20
90
 
91
+ # Module-scoped account ID
92
+ class << self
93
+ attr_accessor :account_id
94
+ end
95
+
21
96
  # Sets the Output logger to use within the client. This can be left uninitializaed
22
97
  # but is useful for debugging.
23
98
  def self.logger=(logger)
@@ -39,5 +114,4 @@ module Sift
39
114
  def self.fatal(msg)
40
115
  @logger.fatal(msg) if @logger
41
116
  end
42
-
43
117
  end
data/sift.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
6
6
  s.name = "sift"
7
7
  s.version = Sift::VERSION
8
8
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["Fred Sadaghiani", "Yoav Schatzberg"]
9
+ s.authors = ["Fred Sadaghiani", "Yoav Schatzberg", "Jacob Burnim"]
10
10
  s.email = ["support@siftscience.com"]
11
11
  s.homepage = "http://siftscience.com"
12
12
  s.summary = %q{Sift Science Ruby API Gem}
@@ -20,8 +20,10 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  # Gems that must be intalled for sift to compile and build
23
- s.add_development_dependency "rspec", ">=2.14.1"
24
- s.add_development_dependency "webmock", ">= 1.16.0"
23
+ s.add_development_dependency "rspec", "~> 3.5"
24
+ s.add_development_dependency "rspec_junit_formatter"
25
+ s.add_development_dependency "pry"
26
+ s.add_development_dependency "webmock", ">= 1.16.0", "< 2"
25
27
 
26
28
  # Gems that must be intalled for sift to work
27
29
  s.add_dependency "httparty", ">= 0.11.0"
@@ -0,0 +1,16 @@
1
+ class FakeDecisions
2
+ def self.index
3
+ {
4
+ data: [
5
+ {
6
+ "id" => "block_user",
7
+ "name" => "Block user"
8
+ },
9
+ {
10
+ "id" => "accept_user",
11
+ "name" => "Accept user"
12
+ }
13
+ ]
14
+ }
15
+ end
16
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,6 @@
1
+ $LOAD_PATH << Dir.pwd
1
2
 
2
3
  require "bundler/setup"
3
- require "sift"
4
4
  require "webmock/rspec"
5
5
 
6
6
  # Setup Fakeweb
@@ -0,0 +1,262 @@
1
+ require_relative "../../../spec_helper"
2
+
3
+ require "sift/client/decision/apply_to"
4
+
5
+ module Sift
6
+ class Client
7
+ class Decision
8
+ describe ApplyTo do
9
+ let(:decision_id) { "block_it" }
10
+ let(:api_key) { "customer_key" }
11
+ let(:decision) { Decision.new(api_key, "account_id") }
12
+
13
+ describe "attributes" do
14
+ ApplyTo::PROPERTIES.each do |attribute|
15
+ it "can read #{attribute} whether it is a symbol or string" do
16
+ expected_value = "right_#{attribute}#{Time.now.to_i}"
17
+ string_hash = {}
18
+ string_hash[attribute] = expected_value
19
+
20
+ applier = ApplyTo.new(api_key, "id", string_hash)
21
+ expect(applier.public_send(attribute)).to(
22
+ eq(expected_value),
23
+ "#{attribute} did not read the right string value"
24
+ )
25
+
26
+ symbol_hash = {}
27
+ symbol_hash[attribute.to_sym] = expected_value
28
+
29
+ applier = ApplyTo.new(api_key, "id", symbol_hash)
30
+ expect(applier.public_send(attribute)).to(
31
+ eq(expected_value),
32
+ "#{attribute} did not read the right symbol value"
33
+ )
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "#run" do
39
+ it "will send a request to block user" do
40
+ configs = {
41
+ source: "manual",
42
+ analyst: "foobar@example.com",
43
+ description: "be blocking errrday allday",
44
+ user_id: "user_1234",
45
+ time: 1234,
46
+ fake_param: "this should not exist"
47
+ }
48
+
49
+ applier = ApplyTo.new(api_key, decision_id, configs)
50
+ request_body = MultiJson.dump({
51
+ source: configs[:source],
52
+ description: configs[:description],
53
+ analyst: configs[:analyst],
54
+ decision_id: decision_id,
55
+ time: 1234
56
+ })
57
+
58
+ response_body = {
59
+ "entity" => {
60
+ "id" => "USER_ID",
61
+ "type" => "user"
62
+ },
63
+ "decision" => {
64
+ "id" => decision_id
65
+ },
66
+ "time" => Time.now.to_i
67
+ }
68
+
69
+ stub_request(:post, put_auth_in_url(api_key, applier.send(:path)))
70
+ .with(body: request_body)
71
+ .to_return(body: MultiJson.dump(response_body))
72
+
73
+ response = applier.run
74
+
75
+ expect(response.ok?).to be(true)
76
+ expect(response.body).to eq(response_body)
77
+ end
78
+
79
+ context "without a valid user_id or order_id" do
80
+ it "will return a response object with the error message" do
81
+ configs = {
82
+ source: "manual",
83
+ analyst: "foobar@example.com",
84
+ description: "be blocking errrday allday",
85
+ decision: decision,
86
+ user_id: "user_1234",
87
+ "order_id" => nil
88
+ }
89
+
90
+ applier = ApplyTo.new(api_key, decision_id, configs)
91
+
92
+ response = applier.run
93
+ non_empty_string_error =
94
+ Validate::Primitive::ERROR_MESSAGES[:non_empty_string]
95
+ error_message = "order_id #{non_empty_string_error}, got NilClass"
96
+
97
+ expect(response.ok?).to be(false)
98
+ expect(response.body["error_message"]).to eq(error_message)
99
+
100
+ ## Invalid user_id
101
+
102
+ configs.delete(:user_id)
103
+ configs.delete("order_id")
104
+
105
+ applier = ApplyTo.new(api_key, decision_id, configs)
106
+
107
+ response = applier.run
108
+ error_message = "user_id #{non_empty_string_error}, got NilClass"
109
+
110
+ expect(response.ok?).to be(false)
111
+ expect(response.body["error_message"]).to eq(error_message)
112
+ end
113
+ end
114
+
115
+ context "without a valid user_id or session_id" do
116
+ it "will return a response object with the error message" do
117
+ configs = {
118
+ source: "manual",
119
+ analyst: "foobar@example.com",
120
+ description: "be blocking errrday allday",
121
+ decision: decision,
122
+ "session_id" => nil,
123
+ user_id: "user_1234"
124
+ }
125
+
126
+ applier = ApplyTo.new(api_key, decision_id, configs)
127
+
128
+ response = applier.run
129
+ non_empty_string_error =
130
+ Validate::Primitive::ERROR_MESSAGES[:non_empty_string]
131
+ error_message = "session_id #{non_empty_string_error}, got NilClass"
132
+
133
+ expect(response.ok?).to be(false)
134
+ expect(response.body["error_message"]).to eq(error_message)
135
+
136
+ ## Invalid user_id
137
+
138
+ configs.delete(:user_id)
139
+ configs.delete("session_id")
140
+
141
+ applier = ApplyTo.new(api_key, decision_id, configs)
142
+
143
+ response = applier.run
144
+ error_message = "user_id #{non_empty_string_error}, got NilClass"
145
+
146
+ expect(response.ok?).to be(false)
147
+ expect(response.body["error_message"]).to eq(error_message)
148
+ end
149
+ end
150
+
151
+ context "when api returns an error code" do
152
+ it "will return a response with the information" do
153
+ configs = {
154
+ source: "manual",
155
+ description: "be blocking errrday allday",
156
+ decision: decision,
157
+ user_id: "user_1234"
158
+ }
159
+
160
+ applier = ApplyTo.new(api_key, decision_id, configs)
161
+ request_body = MultiJson.dump(applier.send(:request_body))
162
+
163
+ response_body = {
164
+ "error" => "not_found",
165
+ "description" => "No decision with id non_existent_decision_id"
166
+ }
167
+
168
+ stub_request(:post, put_auth_in_url(api_key, applier.send(:path)))
169
+ .with(body: request_body)
170
+ .to_return(body: MultiJson.dump(response_body), status: 404)
171
+
172
+ response = applier.run
173
+
174
+ expect(response.ok?).to be(false)
175
+ expect(response.body).to eq(response_body)
176
+ end
177
+ end
178
+ end
179
+
180
+ describe "#run" do
181
+ it "will construct the right path given the configs" do
182
+ user_id = "bad_user@example.com"
183
+ order_id = "ORDER_1235"
184
+
185
+ applier = ApplyTo.new(api_key, decision_id, {
186
+ user_id: user_id,
187
+ account_id: decision.account_id,
188
+ })
189
+
190
+ path = Client::API3_ENDPOINT +
191
+ "/v3/accounts/#{decision.account_id}" +
192
+ "/users/#{CGI.escape(user_id)}" +
193
+ "/decisions"
194
+
195
+ expect("https://api3.siftscience.com/v3/accounts/account_id" +
196
+ "/users/bad_user%40example.com/decisions").to eq(path)
197
+
198
+ applier = ApplyTo.new(api_key, decision_id, {
199
+ user_id: user_id,
200
+ account_id: decision.account_id,
201
+ order_id: order_id
202
+ })
203
+
204
+ path = Client::API3_ENDPOINT +
205
+ "/v3/accounts/#{decision.account_id}/users/" +
206
+ "#{CGI.escape(user_id)}/orders/#{CGI.escape(order_id)}" +
207
+ "/decisions"
208
+
209
+ expect("https://api3.siftscience.com/v3/accounts/account_id" +
210
+ "/users/bad_user%40example.com/orders/ORDER_1235/decisions").to eq(path)
211
+ end
212
+ end
213
+
214
+ describe "#run" do
215
+ it "will construct the right path given the configs" do
216
+ user_id = "bad_user@example.com"
217
+ session_id = "gigtleqddo84l8cm15qe4il"
218
+
219
+ applier = ApplyTo.new(api_key, decision_id, {
220
+ user_id: user_id,
221
+ account_id: decision.account_id,
222
+ })
223
+
224
+ path = Client::API3_ENDPOINT +
225
+ "/v3/accounts/#{decision.account_id}" +
226
+ "/users/#{CGI.escape(user_id)}" +
227
+ "/decisions"
228
+
229
+ expect("https://api3.siftscience.com/v3/accounts/account_id" +
230
+ "/users/bad_user%40example.com/decisions").to eq(path)
231
+
232
+ applier = ApplyTo.new(api_key, decision_id, {
233
+ user_id: user_id,
234
+ account_id: decision.account_id,
235
+ session_id: session_id
236
+ })
237
+
238
+ path = Client::API3_ENDPOINT +
239
+ "/v3/accounts/#{decision.account_id}/users/" +
240
+ "#{CGI.escape(user_id)}/sessions/#{CGI.escape(session_id)}" +
241
+ "/decisions"
242
+
243
+ expect("https://api3.siftscience.com/v3/accounts/account_id" +
244
+ "/users/bad_user%40example.com/sessions/gigtleqddo84l8cm15qe4il/decisions").to eq(path)
245
+ end
246
+ end
247
+
248
+
249
+ # TODO(Kaoru): When we move to webmock 2 we won't need to do this
250
+ # hackery
251
+ #
252
+ # https://github.com/bblimke/webmock/blob/master/CHANGELOG.md#200
253
+ #
254
+ def put_auth_in_url(api_key, url)
255
+ protocal, uri = url.split(/(?<=https\:\/\/)/)
256
+
257
+ protocal + api_key + "@" + uri
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end