sift 1.1.0 → 4.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.
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,3 +1,4 @@
1
1
  module Sift
2
- VERSION = "1.1.0"
2
+ VERSION = "4.0.0"
3
+ API_VERSION = "205"
3
4
  end
data/lib/sift.rb CHANGED
@@ -1,16 +1,69 @@
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
- "/v203/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
- "/v203/users/#{URI.encode(user_id)}/labels"
12
+ # Returns the Score API path for the specified user ID and API version
13
+ def self.score_api_path(user_id, version=API_VERSION)
14
+ "/v#{version}/score/#{ERB::Util.url_encode(user_id)}/"
15
+ end
16
+
17
+ # Returns the User Score API path for the specified user ID and API version
18
+ def self.user_score_api_path(user_id, version=API_VERSION)
19
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/score"
20
+ end
21
+
22
+ # Returns the users API path for the specified user ID and API version
23
+ def self.users_label_api_path(user_id, version=API_VERSION)
24
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/labels"
25
+ end
26
+
27
+ # Returns the path for the Workflow Status API
28
+ def self.workflow_status_path(account_id, run_id)
29
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
30
+ "/workflows/runs/#{ERB::Util.url_encode(run_id)}"
31
+ end
32
+
33
+ # Returns the path for User Decisions API
34
+ def self.user_decisions_api_path(account_id, user_id)
35
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
36
+ "/users/#{ERB::Util.url_encode(user_id)}/decisions"
37
+ end
38
+
39
+ # Returns the path for Orders Decisions API
40
+ def self.order_decisions_api_path(account_id, order_id)
41
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
42
+ "/orders/#{ERB::Util.url_encode(order_id)}/decisions"
43
+ end
44
+
45
+ # Returns the path for Session Decisions API
46
+ def self.session_decisions_api_path(account_id, user_id, session_id)
47
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
48
+ "/users/#{ERB::Util.url_encode(user_id)}" \
49
+ "/sessions/#{ERB::Util.url_encode(session_id)}/decisions"
50
+ end
51
+
52
+ # Returns the path for Content Decisions API
53
+ def self.content_decisions_api_path(account_id, user_id, content_id)
54
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
55
+ "/users/#{ERB::Util.url_encode(user_id)}" \
56
+ "/content/#{ERB::Util.url_encode(content_id)}/decisions"
57
+ end
58
+
59
+ # Module-scoped public API key
60
+ class << self
61
+ attr_accessor :api_key
62
+ end
63
+
64
+ # Module-scoped account ID
65
+ class << self
66
+ attr_accessor :account_id
14
67
  end
15
68
 
16
69
  # Sets the Output logger to use within the client. This can be left uninitializaed
@@ -34,5 +87,4 @@ module Sift
34
87
  def self.fatal(msg)
35
88
  @logger.fatal(msg) if @logger
36
89
  end
37
-
38
90
  end
data/sift.gemspec CHANGED
@@ -6,8 +6,8 @@ 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"]
10
- s.email = ["freds@siftscience.com"]
9
+ s.authors = ["Fred Sadaghiani", "Yoav Schatzberg", "Jacob Burnim"]
10
+ s.email = ["support@siftscience.com"]
11
11
  s.homepage = "http://siftscience.com"
12
12
  s.summary = %q{Sift Science Ruby API Gem}
13
13
  s.description = %q{Sift Science Ruby API. Please see http://siftscience.com for more details.}
@@ -20,13 +20,14 @@ 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 "fakeweb", "~> 1.3.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
- s.add_dependency "httparty", ">= 0.12.0"
28
- s.add_dependency "multi_json", ">= 1.8.2"
29
+ s.add_dependency "httparty", ">= 0.11.0"
30
+ s.add_dependency "multi_json", ">= 1.0"
29
31
 
30
- s.add_development_dependency("rspec", ">= 2.0")
31
32
  s.add_development_dependency("rake")
32
33
  end
@@ -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,10 +1,10 @@
1
+ $LOAD_PATH << Dir.pwd
1
2
 
2
3
  require "bundler/setup"
3
- require "sift"
4
- require "fakeweb"
4
+ require "webmock/rspec"
5
5
 
6
6
  # Setup Fakeweb
7
- FakeWeb.allow_net_connect = false
7
+ WebMock.disable_net_connect!
8
8
 
9
9
  RSpec.configure do |config|
10
10
  end
@@ -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
@@ -0,0 +1,83 @@
1
+ require_relative "../../spec_helper"
2
+ require "multi_json"
3
+
4
+ require "spec/fixtures/fake_responses"
5
+ require "sift/client/decision"
6
+ require "sift/router"
7
+
8
+ module Sift
9
+ class Client
10
+ describe Decision do
11
+ let(:api_key) { "test_api_key" }
12
+ let(:account_id) { "test_account_id" }
13
+ let(:decision) { Decision.new(api_key, account_id) }
14
+
15
+ let(:decision_index_path) {
16
+ # TODO(Kaoru): When we move to webmock 2 we won't need to do this
17
+ # hackery
18
+ #
19
+ # https://github.com/bblimke/webmock/blob/master/CHANGELOG.md#200
20
+ #
21
+ protocal, uri = decision.index_path.split(/(?<=https\:\/\/)/)
22
+
23
+ protocal + api_key + "@" + uri
24
+ }
25
+
26
+ describe "#list" do
27
+ it "will return a response object that is ok" do
28
+ stub_request(:get, decision_index_path)
29
+ .to_return(body: MultiJson.dump(FakeDecisions.index))
30
+
31
+ response = decision.list
32
+
33
+ expect(response.ok?).to be(true)
34
+ expect(response.body["data"])
35
+ .to contain_exactly(*FakeDecisions.index[:data])
36
+ end
37
+
38
+ it "will pass on query params" do
39
+ query_param = {
40
+ abuse_types: %w{promo_abuse content_abuse},
41
+ entity_type: "user",
42
+ limit: 10
43
+ }.inject("?") do |result, (key, value)|
44
+ value = value.join(",") if value.is_a? Array
45
+ "#{result}&#{key}=#{CGI.escape(value.to_s)}"
46
+ end
47
+
48
+ stub_request(:get, "#{decision_index_path}#{query_param}")
49
+ .to_return(body: MultiJson.dump(FakeDecisions.index))
50
+
51
+ response = decision.list({
52
+ limit: 10,
53
+ entity_type: "user",
54
+ abuse_types: %w{promo_abuse content_abuse}
55
+ })
56
+
57
+ expect(response.ok?).to be(true)
58
+ end
59
+
60
+ it "with an unsuccessful response will return a response object" do
61
+ stub_request(:get, decision_index_path)
62
+ .to_return(status: 404, body: "{}")
63
+
64
+ response = decision.list
65
+
66
+ expect(response.ok?).to be(false)
67
+ expect(response.http_status_code).to be(404)
68
+ end
69
+
70
+ it "will fetch next page" do
71
+ next_page = "#{decision.index_path}?from=100"
72
+
73
+ stub_request(:get, "#{decision_index_path}?from=100")
74
+ .to_return(body: MultiJson.dump(FakeDecisions.index))
75
+
76
+ response = decision.list({ "next_ref" => next_page })
77
+
78
+ expect(response.ok?).to be(true)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end