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.
@@ -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