sift 1.1.0 → 4.0.0

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