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.
@@ -1,6 +1,7 @@
1
1
  require 'intercom-rails/script_tag_helper'
2
2
  require 'intercom-rails/auto_include_filter'
3
3
  require 'intercom-rails/config'
4
+ require 'intercom-rails/import'
4
5
  require 'intercom-rails/railtie' if defined? Rails
5
6
 
6
7
  module IntercomRails
@@ -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
@@ -0,0 +1,12 @@
1
+ namespace :intercom do
2
+
3
+ desc "Import your users into intercom"
4
+ task :import => :environment do
5
+ begin
6
+ IntercomRails::Import.run(:status_enabled => true)
7
+ rescue IntercomRails::ImportError => e
8
+ puts e.message
9
+ end
10
+ end
11
+
12
+ end
@@ -7,5 +7,9 @@ module IntercomRails
7
7
  initializer "intercom_on_rails.auto_include_filter.rb" do |app|
8
8
  ActionController::Base.send :after_filter, AutoIncludeFilter
9
9
  end
10
+
11
+ rake_tasks do
12
+ load 'intercom-rails/intercom.rake'
13
+ end
10
14
  end
11
15
  end
@@ -1,3 +1,3 @@
1
1
  module IntercomRails
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  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
- # Your Intercom app_id
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
@@ -1,6 +1,5 @@
1
1
  require 'test_setup'
2
2
 
3
- require 'pry'
4
3
  require 'action_controller'
5
4
  require 'action_controller/test_case'
6
5
 
@@ -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