intercom-rails 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mdown +109 -0
- data/lib/data/cacert.pem +3920 -0
- data/lib/intercom-rails.rb +1 -0
- data/lib/intercom-rails/config.rb +30 -0
- data/lib/intercom-rails/import.rb +153 -0
- data/lib/intercom-rails/intercom.rake +12 -0
- data/lib/intercom-rails/railtie.rb +4 -0
- data/lib/intercom-rails/version.rb +1 -1
- data/lib/rails/generators/intercom/config/config_generator.rb +5 -0
- data/lib/rails/generators/intercom/config/intercom.rb.erb +29 -1
- data/test/action_controller_test_setup.rb +0 -1
- data/test/import_test_setup.rb +62 -0
- data/test/intercom-rails/import_network_test.rb +121 -0
- data/test/intercom-rails/import_unit_test.rb +95 -0
- data/test/test_setup.rb +17 -0
- metadata +62 -6
- data/MIT-LICENSE +0 -21
- data/README.rdoc +0 -73
data/lib/intercom-rails.rb
CHANGED
@@ -2,6 +2,7 @@ module IntercomRails
|
|
2
2
|
|
3
3
|
module Config
|
4
4
|
|
5
|
+
# Your Intercom app_id
|
5
6
|
def self.app_id=(value)
|
6
7
|
@app_id = value
|
7
8
|
end
|
@@ -10,6 +11,25 @@ module IntercomRails
|
|
10
11
|
@app_id
|
11
12
|
end
|
12
13
|
|
14
|
+
# Intercom api secret, for secure mode
|
15
|
+
def self.api_secret=(value)
|
16
|
+
@api_secret = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.api_secret
|
20
|
+
@api_secret
|
21
|
+
end
|
22
|
+
|
23
|
+
# Intercom API key, for some rake tasks
|
24
|
+
def self.api_key=(value)
|
25
|
+
@api_key = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.api_key
|
29
|
+
@api_key
|
30
|
+
end
|
31
|
+
|
32
|
+
# How is the current logged in user accessed in your controllers?
|
13
33
|
def self.current_user=(value)
|
14
34
|
raise ArgumentError, "current_user should be a Proc" unless value.kind_of?(Proc)
|
15
35
|
@current_user = value
|
@@ -19,6 +39,16 @@ module IntercomRails
|
|
19
39
|
@current_user
|
20
40
|
end
|
21
41
|
|
42
|
+
# What class defines your user model?
|
43
|
+
def self.user_model=(value)
|
44
|
+
raise ArgumentError, "user_model should be a Proc" unless value.kind_of?(Proc)
|
45
|
+
@user_model = value
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.user_model
|
49
|
+
@user_model
|
50
|
+
end
|
51
|
+
|
22
52
|
end
|
23
53
|
|
24
54
|
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module IntercomRails
|
6
|
+
class ImportError < StandardError; end
|
7
|
+
class IntercomAPIError < StandardError; end
|
8
|
+
|
9
|
+
class Import
|
10
|
+
|
11
|
+
def self.bulk_create_api_endpoint
|
12
|
+
host = (ENV['INTERCOM_RAILS_DEV'] ? "http://api.intercom.dev" : "https://api.intercom.io")
|
13
|
+
URI.parse(host + "/v1/users/bulk_create")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.run(*args)
|
17
|
+
new(*args).run
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :uri, :http
|
21
|
+
attr_accessor :failed, :total_sent
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
@uri = Import.bulk_create_api_endpoint
|
25
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
26
|
+
@failed = []
|
27
|
+
@total_sent = 0
|
28
|
+
|
29
|
+
@status_enabled = !!options[:status_enabled]
|
30
|
+
|
31
|
+
if uri.scheme == 'https'
|
32
|
+
http.use_ssl = true
|
33
|
+
http.ca_file = File.join(File.dirname(__FILE__), '../data/ca_cert.pem')
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert_runnable
|
39
|
+
raise ImportError, "You can only import your users from your production environment" unless Rails.env.production?
|
40
|
+
raise ImportError, "We couldn't find your user class, please set one in config/initializers/intercom_rails.rb" unless user_klass.present?
|
41
|
+
info "Found user class: #{user_klass}"
|
42
|
+
raise ImportError, "Only ActiveRecord models are supported" unless (user_klass < ActiveRecord::Base)
|
43
|
+
raise ImportError, "Please add an Intercom API Key to config/initializers/intercom.rb" unless IntercomRails.config.api_key.present?
|
44
|
+
info "Intercom API key found"
|
45
|
+
end
|
46
|
+
|
47
|
+
def run
|
48
|
+
assert_runnable
|
49
|
+
|
50
|
+
info "Sending users in batches of #{MAX_BATCH_SIZE}:"
|
51
|
+
batches do |batch, number_in_batch|
|
52
|
+
failures = send_users(batch)['failed']
|
53
|
+
self.failed += failures
|
54
|
+
self.total_sent += number_in_batch
|
55
|
+
|
56
|
+
progress '.' * (number_in_batch - failures.count)
|
57
|
+
progress 'F' * failures.count
|
58
|
+
end
|
59
|
+
info "Successfully created #{self.total_sent - self.failed.count} users", :new_line => true
|
60
|
+
info "Failed to create #{self.failed.count} #{(self.failed.count == 1) ? 'user' : 'users'}, this is likely due to bad data" unless failed.count.zero?
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def total_failed
|
66
|
+
self.failed.count
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
MAX_BATCH_SIZE = 100
|
71
|
+
def batches
|
72
|
+
user_klass.find_in_batches(:batch_size => MAX_BATCH_SIZE) do |users|
|
73
|
+
users_for_wire = users.map { |u| user_for_wire(u) }.compact
|
74
|
+
yield(prepare_batch(users_for_wire), users_for_wire.count) unless users_for_wire.count.zero?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_batch(batch)
|
79
|
+
{:users => batch}.to_json
|
80
|
+
end
|
81
|
+
|
82
|
+
def user_for_wire(user)
|
83
|
+
wired = {}.tap do |h|
|
84
|
+
h[:user_id] = user.id if user.respond_to?(:id) && user.id.present?
|
85
|
+
h[:email] = user.email if user.respond_to?(:email) && user.email.present?
|
86
|
+
h[:name] = user.name if user.respond_to?(:name) && user.name.present?
|
87
|
+
end
|
88
|
+
|
89
|
+
(wired[:user_id] || wired[:email]) ? wired : nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_klass
|
93
|
+
if IntercomRails.config.user_model.present?
|
94
|
+
IntercomRails.config.user_model.call
|
95
|
+
else
|
96
|
+
User
|
97
|
+
end
|
98
|
+
rescue NameError
|
99
|
+
# Rails lazy loads constants, so this is how we check
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_users(users)
|
104
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
105
|
+
request.basic_auth(IntercomRails.config.app_id, IntercomRails.config.api_key)
|
106
|
+
request["Content-Type"] = "application/json"
|
107
|
+
request.body = users
|
108
|
+
|
109
|
+
response = perform_request(request)
|
110
|
+
JSON.parse(response.body)
|
111
|
+
end
|
112
|
+
|
113
|
+
MAX_REQUEST_ATTEMPTS = 3
|
114
|
+
def perform_request(request, attempts = 0, error = {})
|
115
|
+
if (attempts > 0) && (attempts < MAX_REQUEST_ATTEMPTS)
|
116
|
+
sleep(0.5)
|
117
|
+
elsif error.present?
|
118
|
+
raise error[:exception] if error[:exception]
|
119
|
+
raise exception_for_failed_response(error[:failed_response])
|
120
|
+
end
|
121
|
+
|
122
|
+
response = http.request(request)
|
123
|
+
|
124
|
+
return response if successful_response?(response)
|
125
|
+
perform_request(request, attempts + 1, :failed_response => response)
|
126
|
+
rescue Timeout::Error, Errno::ECONNREFUSED => e
|
127
|
+
perform_request(request, attempts + 1, :exception => e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def successful_response?(response)
|
131
|
+
raise ImportError, "App ID or API Key are incorrect, please check them in config/initializers/intercom.rb" if response.code == '403'
|
132
|
+
['200', '201'].include?(response.code)
|
133
|
+
end
|
134
|
+
|
135
|
+
def exception_for_failed_response(response)
|
136
|
+
code = response.code
|
137
|
+
IntercomAPIError.new("The Intercom API request failed with the code: #{code}, after #{MAX_REQUEST_ATTEMPTS} attempts.")
|
138
|
+
end
|
139
|
+
|
140
|
+
def status_enabled?
|
141
|
+
@status_enabled
|
142
|
+
end
|
143
|
+
|
144
|
+
def progress(str)
|
145
|
+
print(str) if status_enabled?
|
146
|
+
end
|
147
|
+
|
148
|
+
def info(str, options = {})
|
149
|
+
puts "#{"\n" if options[:new_line]}* #{str}" if status_enabled?
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -7,9 +7,14 @@ module Intercom
|
|
7
7
|
end
|
8
8
|
|
9
9
|
argument :app_id, :desc => "Your Intercom app-id, which can be found here: https://www.intercom.io/apps/api_keys"
|
10
|
+
argument :api_secret, :desc => "Your Intercom api-secret, used for secure mode"
|
11
|
+
argument :api_key, :desc => "An Intercom API key, for various rake tasks"
|
10
12
|
|
11
13
|
def create_config_file
|
12
14
|
@app_id = app_id
|
15
|
+
@api_secret = api_secret
|
16
|
+
@api_key = api_key
|
17
|
+
|
13
18
|
template("intercom.rb.erb", "config/initializers/intercom.rb")
|
14
19
|
end
|
15
20
|
|
@@ -1,8 +1,36 @@
|
|
1
1
|
IntercomRails.config do |config|
|
2
|
-
#
|
2
|
+
# == Intercom app_id
|
3
|
+
#
|
3
4
|
config.app_id = "<%= @app_id %>"
|
4
5
|
|
6
|
+
# == Intercom secret key
|
7
|
+
# This is reuqired to enable secure mode, you can find it on your Intercom
|
8
|
+
# "security" configuration page.
|
9
|
+
#
|
10
|
+
<%- if @api_secret -%>
|
11
|
+
config.api_secret = "<%= @api_secret %>"
|
12
|
+
<%- else -%>
|
13
|
+
# config.api_secret = '...'
|
14
|
+
<%- end -%>
|
15
|
+
|
16
|
+
# == Intercom API Key
|
17
|
+
# This is required for some Intercom rake tasks like importing your users;
|
18
|
+
# you can generate one at https://www.intercom.io/apps/api_keys.
|
19
|
+
#
|
20
|
+
<%- if @api_key -%>
|
21
|
+
config.api_key = "<%= @api_key %>"
|
22
|
+
<%- else -%>
|
23
|
+
# config.api_key = "..."
|
24
|
+
<%- end -%>
|
25
|
+
|
26
|
+
# == Curent user name
|
5
27
|
# The method/variable that contains the logged in user in your controllers.
|
6
28
|
# If it is `current_user` or `@user`, then you can ignore this
|
29
|
+
#
|
7
30
|
# config.current_user = Proc.new { current_user }
|
31
|
+
|
32
|
+
# == User model class
|
33
|
+
# The class which defines your user model
|
34
|
+
#
|
35
|
+
# config.user_model = Proc.new { User }
|
8
36
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_setup'
|
2
|
+
require 'active_support/string_inquirer'
|
3
|
+
|
4
|
+
class Rails
|
5
|
+
def self.env
|
6
|
+
ActiveSupport::StringInquirer.new("production")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ActiveRecord
|
11
|
+
class Base; end
|
12
|
+
end
|
13
|
+
|
14
|
+
class User
|
15
|
+
|
16
|
+
attr_reader :id, :email, :name
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
options.each do |k,v|
|
20
|
+
instance_variable_set(:"@#{k}", v)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
MOCK_USERS = [
|
25
|
+
{:id => 1, :email => "ben@intercom.io", :name => "Ben McRedmond"},
|
26
|
+
{:id => 2, :email => "ciaran@intercom.io", :name => "Ciaran Lee"}
|
27
|
+
]
|
28
|
+
|
29
|
+
def self.find_in_batches(*args)
|
30
|
+
yield(MOCK_USERS.map {|u| new(u)})
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all
|
34
|
+
MOCK_USERS.map { |u| new(u) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.first
|
38
|
+
new(MOCK_USERS.first)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.<(other)
|
42
|
+
other == ActiveRecord::Base
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
module ImportTest
|
48
|
+
|
49
|
+
def setup
|
50
|
+
super
|
51
|
+
IntercomRails.config.stub(:api_key).and_return("abcd")
|
52
|
+
end
|
53
|
+
|
54
|
+
def teardown
|
55
|
+
super
|
56
|
+
Rails.rspec_reset
|
57
|
+
User.rspec_reset
|
58
|
+
IntercomRails::Import.rspec_reset
|
59
|
+
IntercomRails::Import.unstub_all_instance_methods
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'import_test_setup'
|
2
|
+
require 'sinatra/base'
|
3
|
+
|
4
|
+
class MockIntercom < Sinatra::Base
|
5
|
+
|
6
|
+
set :server, 'thin'
|
7
|
+
|
8
|
+
before do
|
9
|
+
content_type 'application/json'
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/health_check' do
|
13
|
+
content_type 'plain/text'
|
14
|
+
'hello world'
|
15
|
+
end
|
16
|
+
|
17
|
+
post '/all_successful' do
|
18
|
+
{:failed => []}.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
post '/one_failure' do
|
22
|
+
{:failed => ['ben@intercom.io']}.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/bad_auth' do
|
26
|
+
status 403
|
27
|
+
{"error" => {"type" => "not_authenticated", "message" => "HTTP Basic: Access denied."}}.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
post '/500_error' do
|
31
|
+
status 500
|
32
|
+
{"error" => {"type" => "server_error", "message" => "Danger deploy, gone wrong?"}}.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class InterRunner < MiniTest::Unit
|
38
|
+
|
39
|
+
self.runner = self.new
|
40
|
+
|
41
|
+
def _run(*args)
|
42
|
+
@mock_intercom_pid = start_mock_intercom
|
43
|
+
super
|
44
|
+
ensure
|
45
|
+
Process.kill('INT', @mock_intercom_pid)
|
46
|
+
Process.wait(@mock_intercom_pid) rescue SystemError
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def start_mock_intercom
|
52
|
+
pid = fork do
|
53
|
+
MockIntercom.run!(:port => 46837) do |server|
|
54
|
+
server.silent = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
response = nil
|
59
|
+
uri = URI.parse("http://localhost:46837/health_check")
|
60
|
+
|
61
|
+
begin
|
62
|
+
response = Net::HTTP.get_response(uri).body until(response == 'hello world')
|
63
|
+
rescue Errno::ECONNREFUSED
|
64
|
+
sleep(0.5)
|
65
|
+
retry
|
66
|
+
end
|
67
|
+
|
68
|
+
pid
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
class ImportNetworkTest < InterRunner::TestCase
|
74
|
+
|
75
|
+
include ImportTest
|
76
|
+
|
77
|
+
def api_path=(path)
|
78
|
+
IntercomRails::Import.stub(:bulk_create_api_endpoint) {
|
79
|
+
URI.parse("http://localhost:46837/#{path}")
|
80
|
+
}
|
81
|
+
|
82
|
+
@import = IntercomRails::Import.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_empty_failed
|
86
|
+
self.api_path = '/all_successful'
|
87
|
+
|
88
|
+
@import.run
|
89
|
+
assert_equal [], @import.failed
|
90
|
+
assert_equal 2, @import.total_sent
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_sets_failed_correctly
|
94
|
+
self.api_path = '/one_failure'
|
95
|
+
|
96
|
+
@import.run
|
97
|
+
assert_equal ["ben@intercom.io"], @import.failed
|
98
|
+
assert_equal 2, @import.total_sent
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_raises_import_error_on_bad_auth
|
102
|
+
self.api_path = '/bad_auth'
|
103
|
+
|
104
|
+
exception = assert_raises(IntercomRails::ImportError) {
|
105
|
+
@import.run
|
106
|
+
}
|
107
|
+
|
108
|
+
assert_equal "App ID or API Key are incorrect, please check them in config/initializers/intercom.rb", exception.message
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_throws_exception_when_intercom_api_is_being_a_dick
|
112
|
+
self.api_path = '/500_error'
|
113
|
+
|
114
|
+
exception = assert_raises(IntercomRails::IntercomAPIError) {
|
115
|
+
@import.run
|
116
|
+
}
|
117
|
+
|
118
|
+
assert_equal "The Intercom API request failed with the code: 500, after 3 attempts.", exception.message
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|