From 3c724d7f244a947707de99750a6886fa1f1c7d2d Mon Sep 17 00:00:00 2001 From: RickR Date: Thu, 13 Jul 2017 21:54:09 -0400 Subject: [PATCH 1/2] Adds send grid job. Fixes #49 --- Gemfile | 1 + Gemfile.lock | 4 ++ app/jobs/add_user_to_send_grid_job.rb | 7 ++++ app/lib/send_grid_client.rb | 35 ++++++++++++++++ app/lib/send_grid_client/response.rb | 26 ++++++++++++ app/models/user.rb | 8 ++++ run/secrets/sendgrid_api_key | 1 + test/jobs/add_user_to_send_grid_job_test.rb | 9 +++++ test/lib/send_grid_client_test.rb | 45 +++++++++++++++++++++ test/models/user_test.rb | 1 + 10 files changed, 137 insertions(+) create mode 100644 app/jobs/add_user_to_send_grid_job.rb create mode 100644 app/lib/send_grid_client.rb create mode 100644 app/lib/send_grid_client/response.rb create mode 100644 run/secrets/sendgrid_api_key create mode 100644 test/jobs/add_user_to_send_grid_job_test.rb create mode 100644 test/lib/send_grid_client_test.rb diff --git a/Gemfile b/Gemfile index 1d11ff8e..0b978ef6 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'pundit' gem 'rack-cors' gem 'rails', '~> 5.0.2' gem 'redis', '~> 3.0' +gem 'sendgrid-ruby' gem 'sentry-raven' gem 'sidekiq' diff --git a/Gemfile.lock b/Gemfile.lock index c39bfbdb..085d1017 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,6 +156,9 @@ GEM redis (3.3.3) responders (2.3.0) railties (>= 4.2.0, < 5.1) + ruby_http_client (3.0.2) + sendgrid-ruby (4.0.8) + ruby_http_client (~> 3.0.1) sentry-raven (2.5.3) faraday (>= 0.7.6, < 1.0) sidekiq (4.2.10) @@ -210,6 +213,7 @@ DEPENDENCIES rack-cors rails (~> 5.0.2) redis (~> 3.0) + sendgrid-ruby sentry-raven sidekiq spring diff --git a/app/jobs/add_user_to_send_grid_job.rb b/app/jobs/add_user_to_send_grid_job.rb new file mode 100644 index 00000000..3f3bfd2f --- /dev/null +++ b/app/jobs/add_user_to_send_grid_job.rb @@ -0,0 +1,7 @@ +class AddUserToSendGridJob < ApplicationJob + queue_as :default + + def perform(user) + SendGridClient.new.add_user(user) + end +end diff --git a/app/lib/send_grid_client.rb b/app/lib/send_grid_client.rb new file mode 100644 index 00000000..85fb18a8 --- /dev/null +++ b/app/lib/send_grid_client.rb @@ -0,0 +1,35 @@ +class SendGridClient + LIST_ID = 1581794 + + class SendGridClientError < StandardError; end + + def initialize + @sendgrid = SendGrid::API.new(api_key: OperationCode.fetch_secret_with(name: :sendgrid_api_key)) + self + end + + def add_user(user) + Rails.logger.info("Adding user #{user.email} (#{user.id})") + recipient_id = add_recipient(user.attributes.slice(*User::SENDGRID_ATTRIBUTES)) + add_recipient_to_list(recipient_id) + rescue => e + Rails.logger.error "Failed to add user #{user.id}: #{e}" + Rails.logger.error e.backtrace.join("\n") + end + + private + + def add_recipient(data) + response = Response.new(@sendgrid.client.contactdb.recipients.post(request_body: [data])) + Rails.logger.info("Response: #{response.body}") + raise(SendGridClientError, response.body) unless response.successful? + response.body['persisted_recipients'] + end + + def add_recipient_to_list(recipient_id) + response = Response.new(@sendgrid.client.contactdb.lists._(LIST_ID).recipients._(recipient_id).post()) + Rails.logger.info("Response: #{response.body}") + raise(SendGridClientError, response.body) unless response.successful? + true + end +end diff --git a/app/lib/send_grid_client/response.rb b/app/lib/send_grid_client/response.rb new file mode 100644 index 00000000..4b20a667 --- /dev/null +++ b/app/lib/send_grid_client/response.rb @@ -0,0 +1,26 @@ +class SendGridClient::Response + attr_reader :body, :status_code + + def initialize(response) + @response = response + @body = JSON.parse(response.body) unless response.body.blank? + @status_code = response.status_code + end + + def successful? + successful_status_code && successful_error_count + end + + private + + def successful_status_code + status_code == '201' + end + + # Some requests don't return a body so we default to just checking + # the status code + def successful_error_count + return true if body.blank? + body['error_count'] == 0 + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 0d05dcc1..e8d89bab 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,9 @@ class User < ApplicationRecord :recoverable, :rememberable, :trackable, :validatable geocoded_by :zip + # These attributes are what we send to sendgrid as custom fields + SENDGRID_ATTRIBUTES = %w[first_name last_name email] + after_create :welcome_user before_save :geocode, if: ->(v) { v.zip.present? && v.zip_changed? } @@ -20,6 +23,7 @@ def welcome_user invite_to_slack add_to_mailchimp add_to_airtables + add_to_send_grid end def invite_to_slack @@ -34,6 +38,10 @@ def add_to_airtables AddUserToAirtablesJob.perform_later(self) end + def add_to_send_grid + AddUserToSendGridJob.perform_later(self) + end + def token JsonWebToken.encode(user_id: self.id, roles: []) end diff --git a/run/secrets/sendgrid_api_key b/run/secrets/sendgrid_api_key new file mode 100644 index 00000000..dfe4f861 --- /dev/null +++ b/run/secrets/sendgrid_api_key @@ -0,0 +1 @@ +test_sendgrid_api_key diff --git a/test/jobs/add_user_to_send_grid_job_test.rb b/test/jobs/add_user_to_send_grid_job_test.rb new file mode 100644 index 00000000..8b60985e --- /dev/null +++ b/test/jobs/add_user_to_send_grid_job_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class AddUserToSendGridJobTest < ActiveJob::TestCase + test 'it adds a user to send grid' do + user = FactoryGirl.build(:user) + SendGridClient.any_instance.expects(:add_user).with(user) + AddUserToSendGridJob.perform_now(user) + end +end diff --git a/test/lib/send_grid_client_test.rb b/test/lib/send_grid_client_test.rb new file mode 100644 index 00000000..8cb78c55 --- /dev/null +++ b/test/lib/send_grid_client_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class SendGridClientTest < ActiveSupport::TestCase + test 'it adds a user to sendgrid' do + user = FactoryGirl.build(:user) + mock_sendgrid_client_for(user) + assert SendGridClient.new.add_user(user) + end + + # Sendgrid's gem uses a TON of chained methods. This mess is stubbing out all of them + # while checking to make sure we get sane parameters + def mock_sendgrid_client_for(user) + mock_response = mock + mock_response.expects(:status_code).returns('201').at_least_once + mock_response.stubs(:body).returns('{ "error_count": 0, "persisted_recipients": [ "1234" ] }') + + mock_recipients = mock + mock_recipients.expects(:post).with(request_body: ['first_name' => user.first_name, 'last_name' => user.last_name, 'email' => user.email]).returns(mock_response) + + mock_list_response = mock + mock_list_response.stubs(:status_code).returns('201') + mock_list_response.stubs(:body).returns(nil) + + mock_list_post = mock + mock_list_post.stubs(:post).returns(mock_list_response) + + mock_recipient_id = mock + mock_recipient_id.stubs(:_).with(['1234']).returns(mock_list_post) + + mock_list_id = mock + mock_list_id.stubs(:recipients).returns(mock_recipient_id) + + mock_lists = mock + mock_lists.stubs(:_).with(SendGridClient::LIST_ID).returns(mock_list_id) + + mock_contactdb = mock + mock_contactdb.stubs(:recipients).returns(mock_recipients) + mock_contactdb.stubs(:lists).returns(mock_lists) + + mock_client = mock + mock_client.stubs(:contactdb).returns(mock_contactdb) + + SendGrid::API.any_instance.stubs(:client).returns(mock_client) + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 17ab6d34..7f29581b 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -11,6 +11,7 @@ def setup SlackJobs::InviterJob.expects(:perform_later).with(user_opts[:email]) MailchimpInviterJob.expects(:perform_later).with(email: user_opts[:email]) AddUserToAirtablesJob.expects(:perform_later).with(user) + AddUserToSendGridJob.expects(:perform_later).with(user) assert_difference('User.count') { user.save } end From 47eab999be3642d6f17b67c30d0beb3dcfbfeb17 Mon Sep 17 00:00:00 2001 From: RickR Date: Fri, 14 Jul 2017 16:03:56 -0400 Subject: [PATCH 2/2] PR fixes --- app/lib/send_grid_client.rb | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/lib/send_grid_client.rb b/app/lib/send_grid_client.rb index 85fb18a8..2fcb4b62 100644 --- a/app/lib/send_grid_client.rb +++ b/app/lib/send_grid_client.rb @@ -4,7 +4,7 @@ class SendGridClient class SendGridClientError < StandardError; end def initialize - @sendgrid = SendGrid::API.new(api_key: OperationCode.fetch_secret_with(name: :sendgrid_api_key)) + @sendgrid = SendGrid::API.new(api_key: api_key) self end @@ -20,16 +20,31 @@ def add_user(user) private def add_recipient(data) - response = Response.new(@sendgrid.client.contactdb.recipients.post(request_body: [data])) + response = Response.new(add_recipient_request(data)) Rails.logger.info("Response: #{response.body}") raise(SendGridClientError, response.body) unless response.successful? response.body['persisted_recipients'] end def add_recipient_to_list(recipient_id) - response = Response.new(@sendgrid.client.contactdb.lists._(LIST_ID).recipients._(recipient_id).post()) + response = Response.new(add_recipient_to_list_request(recipient_id)) Rails.logger.info("Response: #{response.body}") raise(SendGridClientError, response.body) unless response.successful? true end + + def api_key + OperationCode.fetch_secret_with(name: :sendgrid_api_key) + end + + # The method chains in these request methods represents the path of the API request. + # SendGrid calls this it's Fluent Interface + # https://github.com/sendgrid/sendgrid-ruby#general-v3-web-api-usage-with-fluent-interface + def add_recipient_request(data) + @sendgrid.client.contactdb.recipients.post(request_body: [data]) + end + + def add_recipient_to_list_request(recipient_id) + @sendgrid.client.contactdb.lists._(LIST_ID).recipients._(recipient_id).post() + end end