Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Update PostmarkWebhookController and spec it
Also document the environment variables that Postmark depends on.
  • Loading branch information
harigopal committed Sep 5, 2019
1 parent 24ee827 commit d851721
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 9 deletions.
21 changes: 13 additions & 8 deletions app/controllers/users/postmark_webhook_controller.rb
Expand Up @@ -3,21 +3,26 @@ class PostmarkWebhookController < ApplicationController
skip_before_action :verify_authenticity_token
http_basic_authenticate_with name: ENV['POSTMARK_HOOK_ID'], password: ENV['POSTMARK_HOOK_SECRET']

# POST /users/email_bounce
def email_bounce
@user = current_school&.users&.find_by(email: params[:Email])
return unless @user.present? && params[:Type].in?(target_bounce_types)

mark_user_unemailable
mark_users_bounced if users.exists? && params[:Type].in?(accepted_webhook_types)
head :ok
end

private

def target_bounce_types
%w[HardBounce BadEmailAddress SpamComplaint]
def users
@users ||= User.with_email(params[:Email])
end

def accepted_webhook_types
%w[HardBounce SpamComplaint]
end

def mark_user_unemailable
@user.update!(email_bounced_at: params[:BouncedAt], email_bounce_type: params[:Type])
def mark_users_bounced
users.each do |user|
user.update!(email_bounced_at: Time.zone.now, email_bounce_type: params[:Type])
end
end
end
end
5 changes: 5 additions & 0 deletions example.env
Expand Up @@ -39,6 +39,11 @@ FACEBOOK_SECRET=secret_for_facebook_oauth
GOOGLE_OAUTH2_CLIENT_ID=client_id_for_google_oauth
GOOGLE_OAUTH2_CLIENT_SECRET=client_secret_for_google_oauth

# Postmark mailing service.
POSTMARK_API_TOKEN=api_token_from_postmark
POSTMARK_HOOK_ID=bounce_webhook_username_from_postmark
POSTMARK_HOOK_SECRET=bounce_webhook_password_from_postmark

# Domain which has been configured for re-direction after SSO with OAuth.
SSO_DOMAIN=sso.school.localhost

Expand Down
2 changes: 1 addition & 1 deletion spec/factories/user.rb
Expand Up @@ -2,7 +2,7 @@
factory :user do
email { Faker::Internet.email(name) }
name { Faker::Name.name }
school_id { School.find_by(name: 'test')&.id || create(:school, :current).id }
school { School.find_by(name: 'test') || create(:school, :current) }
title { Faker::Lorem.words(3).join(' ') }
end
end
90 changes: 90 additions & 0 deletions spec/requests/users/postmark_webhook_bounce_reports_spec.rb
@@ -0,0 +1,90 @@
require 'rails_helper'

describe 'Postmark webhook bounce reports' do
include HttpBasicAuthHelper

let(:user_1) { create :user }
let!(:user_2) { create :user }
let(:another_school) { create :school }
let!(:user_1_s2) { create :user, email: user_1.email, school: another_school }

before(:all) do
ENV['POSTMARK_HOOK_ID'] = 'hook_id_for_test'
ENV['POSTMARK_HOOK_SECRET'] = 'hook_secret_for_test'
@headers = request_spec_headers('hook_id_for_test', 'hook_secret_for_test')
end

context 'when postmark reports hard-bounce for a user in multiple schools' do
it 'marks all matching users in all schools as hard-bounced' do
expect do
post '/users/email_bounce', params: { Email: user_1.email, Type: 'HardBounce' }, headers: @headers
end.to change { user_1.reload.email_bounced_at }.from(nil)

expect(response.code).to eq("200")

# Both users in different schools with the reported email should be marked bounced.
expect(user_1.email_bounce_type).to eq('HardBounce')
expect(user_1_s2.reload.email_bounced_at).not_to eq(nil)
expect(user_1_s2.email_bounce_type).to eq('HardBounce')

# User with different email should have been left alone.
expect(user_2.reload.email_bounced_at).to eq(nil)
expect(user_2.email_bounce_type).to eq(nil)
end
end

context 'when postmark reports spam-complaint from a user in multiple schools' do
it 'marks all matching users in all schools as bounced with appropriate bounce type' do
post '/users/email_bounce', params: { Email: user_1.email, Type: 'SpamComplaint' }, headers: @headers

expect(response.code).to eq("200")

# Both users in different schools with the reported email should be marked bounced with the correct bounce type.
expect(user_1.reload.email_bounced_at).not_to eq(nil)
expect(user_1.email_bounce_type).to eq('SpamComplaint')
expect(user_1_s2.reload.email_bounced_at).not_to eq(nil)
expect(user_1_s2.email_bounce_type).to eq('SpamComplaint')

# User with different email should have been left alone.
expect(user_2.reload.email_bounced_at).to eq(nil)
expect(user_2.email_bounce_type).to eq(nil)
end
end

context 'when postmark reports some complaint from a user who is not registered' do
it 'ignores the request' do
post '/users/email_bounce', params: { Email: "missinguser@example.com", Type: 'HardBounce' }, headers: @headers
expect(response.code).to eq("200")

# None of the users should have been marked bounced.
expect(user_1.reload.email_bounced_at).to eq(nil)
expect(user_1_s2.reload.email_bounced_at).to eq(nil)
expect(user_2.reload.email_bounced_at).to eq(nil)
end
end

context 'when postmark reports a webhook of a type that is unhandled' do
it 'ignores the request' do
post '/users/email_bounce', params: { Email: user_1.email, Type: 'Delivery' }, headers: @headers

expect(response.code).to eq("200")

# None of the users should have been marked bounced.
expect(user_1.reload.email_bounced_at).to eq(nil)
expect(user_1_s2.reload.email_bounced_at).to eq(nil)
expect(user_2.reload.email_bounced_at).to eq(nil)
end
end

context 'when postmark reports some complaint with incorrect credentials' do
it 'rejects the request' do
post '/users/email_bounce', params: { Email: user_1.email, Type: 'HardBounce' }
expect(response.code).to eq("401")

# None of the users should have been marked bounced.
expect(user_1.reload.email_bounced_at).to eq(nil)
expect(user_1_s2.reload.email_bounced_at).to eq(nil)
expect(user_2.reload.email_bounced_at).to eq(nil)
end
end
end
8 changes: 8 additions & 0 deletions spec/support/spec_helpers/http_basic_auth_helper.rb
@@ -0,0 +1,8 @@
# Helper for managing HTTP Basic Auth when making requests.
module HttpBasicAuthHelper
def request_spec_headers(user, password)
{
'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
}
end
end

0 comments on commit d851721

Please sign in to comment.