intercom-rails 0.0.7 → 0.0.8

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