sift 2.0.0.0 → 2.1.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.
- checksums.yaml +4 -4
- data/README.md +225 -0
- data/lib/sift.rb +2 -2
- data/lib/sift/client.rb +56 -13
- data/lib/sift/client/decision.rb +67 -0
- data/lib/sift/client/decision/apply_to.rb +104 -0
- data/lib/sift/error.rb +13 -0
- data/lib/sift/router.rb +41 -0
- data/lib/sift/utils/hash_getter.rb +15 -0
- data/lib/sift/validate/decision.rb +53 -0
- data/lib/sift/validate/primitive.rb +43 -0
- data/lib/sift/version.rb +1 -1
- data/sift.gemspec +2 -1
- data/spec/fixtures/fake_responses.rb +16 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/client/decision/apply_to_spec.rb +183 -0
- data/spec/unit/client/decision_spec.rb +83 -0
- data/spec/unit/client_203_spec.rb +2 -1
- data/spec/unit/client_label_spec.rb +2 -3
- data/spec/unit/client_spec.rb +3 -3
- data/spec/unit/router_spec.rb +37 -0
- data/spec/unit/validate/decision_spec.rb +85 -0
- data/spec/unit/validate/primitive_spec.rb +73 -0
- metadata +39 -8
- data/README.rdoc +0 -110
- data/spec/unit/sift_spec.rb +0 -6
data/lib/sift/error.rb
ADDED
data/lib/sift/router.rb
ADDED
@@ -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,53 @@
|
|
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_user?
|
19
|
+
run do
|
20
|
+
validate_key(:non_empty_string, :user_id)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def run
|
27
|
+
clear_errors!
|
28
|
+
yield
|
29
|
+
error_messages.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_primitive
|
33
|
+
Validate::Primitive
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_key(type, *keys)
|
37
|
+
keys.each do |key|
|
38
|
+
if error_message = validate_primitive.public_send(type, get(key))
|
39
|
+
error_messages << "#{key} #{error_message}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear_errors!
|
45
|
+
@error_messages = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(key)
|
49
|
+
configs[key] || configs[key.to_s]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
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
data/sift.gemspec
CHANGED
@@ -20,7 +20,8 @@ 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", "
|
23
|
+
s.add_development_dependency "rspec", "~> 3.5"
|
24
|
+
s.add_development_dependency "pry"
|
24
25
|
s.add_development_dependency "webmock", ">= 1.16.0", "< 2"
|
25
26
|
|
26
27
|
# Gems that must be intalled for sift to work
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,183 @@
|
|
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
|
+
decision: decision,
|
45
|
+
user_id: "user_1234"
|
46
|
+
}
|
47
|
+
|
48
|
+
applier = ApplyTo.new(api_key, decision_id, configs)
|
49
|
+
request_body = MultiJson.dump(applier.send(:request_body))
|
50
|
+
|
51
|
+
response_body = {
|
52
|
+
"entity" => {
|
53
|
+
"id" => "USER_ID",
|
54
|
+
"type" => "user"
|
55
|
+
},
|
56
|
+
"decision" => {
|
57
|
+
"id" => decision_id
|
58
|
+
},
|
59
|
+
"time" => Time.now.to_i
|
60
|
+
}
|
61
|
+
|
62
|
+
stub_request(:post, put_auth_in_url(api_key, applier.send(:path)))
|
63
|
+
.with(body: request_body)
|
64
|
+
.to_return(body: MultiJson.dump(response_body))
|
65
|
+
|
66
|
+
response = applier.run
|
67
|
+
|
68
|
+
expect(response.ok?).to be(true)
|
69
|
+
expect(response.body).to eq(response_body)
|
70
|
+
end
|
71
|
+
|
72
|
+
context "without a valid user_id or order_id" do
|
73
|
+
it "will return a response object with the error message" do
|
74
|
+
configs = {
|
75
|
+
source: "manual",
|
76
|
+
analyst: "foobar@example.com",
|
77
|
+
description: "be blocking errrday allday",
|
78
|
+
decision: decision,
|
79
|
+
user_id: "user_1234",
|
80
|
+
"order_id" => nil
|
81
|
+
}
|
82
|
+
|
83
|
+
applier = ApplyTo.new(api_key, decision_id, configs)
|
84
|
+
|
85
|
+
response = applier.run
|
86
|
+
non_empty_string_error =
|
87
|
+
Validate::Primitive::ERROR_MESSAGES[:non_empty_string]
|
88
|
+
error_message = "order_id #{non_empty_string_error}, got NilClass"
|
89
|
+
|
90
|
+
expect(response.ok?).to be(false)
|
91
|
+
expect(response.body["error_message"]).to eq(error_message)
|
92
|
+
|
93
|
+
## Invalid user_id
|
94
|
+
|
95
|
+
configs.delete(:user_id)
|
96
|
+
configs.delete("order_id")
|
97
|
+
|
98
|
+
applier = ApplyTo.new(api_key, decision_id, configs)
|
99
|
+
|
100
|
+
response = applier.run
|
101
|
+
error_message = "user_id #{non_empty_string_error}, got NilClass"
|
102
|
+
|
103
|
+
expect(response.ok?).to be(false)
|
104
|
+
expect(response.body["error_message"]).to eq(error_message)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "when api returns an error code" do
|
109
|
+
it "will return a response with the information" do
|
110
|
+
configs = {
|
111
|
+
source: "manual",
|
112
|
+
description: "be blocking errrday allday",
|
113
|
+
decision: decision,
|
114
|
+
user_id: "user_1234"
|
115
|
+
}
|
116
|
+
|
117
|
+
applier = ApplyTo.new(api_key, decision_id, configs)
|
118
|
+
request_body = MultiJson.dump(applier.send(:request_body))
|
119
|
+
|
120
|
+
response_body = {
|
121
|
+
"error" => "not_found",
|
122
|
+
"description" => "No decision with id non_existent_decision_id"
|
123
|
+
}
|
124
|
+
|
125
|
+
stub_request(:post, put_auth_in_url(api_key, applier.send(:path)))
|
126
|
+
.with(body: request_body)
|
127
|
+
.to_return(body: MultiJson.dump(response_body), status: 404)
|
128
|
+
|
129
|
+
response = applier.run
|
130
|
+
|
131
|
+
expect(response.ok?).to be(false)
|
132
|
+
expect(response.body).to eq(response_body)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "private#path" do
|
138
|
+
it "will construct the right path given the configs" do
|
139
|
+
user_id = "bad_user@example.com"
|
140
|
+
order_id = "ORDER_1235"
|
141
|
+
|
142
|
+
applier = ApplyTo.new(api_key, decision_id, {
|
143
|
+
user_id: user_id,
|
144
|
+
account_id: decision.account_id,
|
145
|
+
})
|
146
|
+
|
147
|
+
path = Client::API3_ENDPOINT +
|
148
|
+
"/v3/accounts/#{decision.account_id}" +
|
149
|
+
"/users/#{CGI.escape(user_id)}" +
|
150
|
+
"/decisions"
|
151
|
+
|
152
|
+
|
153
|
+
expect(applier.send(:path)).to eq(path)
|
154
|
+
|
155
|
+
applier = ApplyTo.new(api_key, decision_id, {
|
156
|
+
user_id: user_id,
|
157
|
+
account_id: decision.account_id,
|
158
|
+
order_id: order_id
|
159
|
+
})
|
160
|
+
|
161
|
+
path = Client::API3_ENDPOINT +
|
162
|
+
"/v3/accounts/#{decision.account_id}/users/" +
|
163
|
+
"#{CGI.escape(user_id)}/orders/#{CGI.escape(order_id)}" +
|
164
|
+
"/decisions"
|
165
|
+
|
166
|
+
expect(applier.send(:path)).to eq(path)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# TODO(Kaoru): When we move to webmock 2 we won't need to do this
|
171
|
+
# hackery
|
172
|
+
#
|
173
|
+
# https://github.com/bblimke/webmock/blob/master/CHANGELOG.md#200
|
174
|
+
#
|
175
|
+
def put_auth_in_url(api_key, url)
|
176
|
+
protocal, uri = url.split(/(?<=https\:\/\/)/)
|
177
|
+
|
178
|
+
protocal + api_key + "@" + uri
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
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
|