sift 2.0.0.0 → 2.1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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
@@ -1,4 +1,4 @@
1
1
  module Sift
2
- VERSION = "2.0.0.0"
2
+ VERSION = "2.1.0.0"
3
3
  API_VERSION = "204"
4
4
  end
@@ -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", ">=2.14.1"
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
@@ -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
@@ -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,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