Skip to content

Commit 9434509

Browse files
committed
feat: lock down success and log
1 parent e9b1852 commit 9434509

2 files changed

Lines changed: 116 additions & 7 deletions

File tree

app/services/subscriptions/pardot_form_handler_submitter.rb

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ class PardotFormHandlerSubmitter
66

77
REQUEST_TIMEOUT_SECONDS = 10
88
OPEN_TIMEOUT_SECONDS = 5
9+
SUCCESS_STATUS_CODES = [200, 302].freeze
10+
SUCCESS_LOCATION_PATTERNS = ['/success'].freeze
11+
ERROR_LOCATION_PATTERNS = ['/error'].freeze
912

1013
def initialize(endpoint_url:)
1114
@endpoint_url = endpoint_url
@@ -15,14 +18,12 @@ def call(form_payload:)
1518
return missing_configuration_result if endpoint_url.blank?
1619

1720
response = faraday.post(endpoint_url, form_payload)
18-
return Result.new(success?: true) if response.success?
19-
20-
Result.new(
21-
success?: false,
22-
status: :bad_gateway,
23-
error_code: 'subscription_provider_rejected',
24-
message: 'Subscription provider rejected the request.'
21+
Rails.logger.info(
22+
"[subscriptions#provider] status=#{response.status} " \
23+
"location=#{redirect_location(response)} " \
24+
"classification=#{classification_for(response)}"
2525
)
26+
classify_response(response)
2627
rescue Faraday::Error
2728
# Sentry.capture_exception(e)
2829
Result.new(
@@ -53,5 +54,55 @@ def missing_configuration_result
5354
message: 'Subscription provider endpoint is not configured.'
5455
)
5556
end
57+
58+
def classify_response(response)
59+
return reject_result unless SUCCESS_STATUS_CODES.include?(response.status)
60+
return Result.new(success?: true) if response.status == 200
61+
return reject_result if redirect_to_error_location?(response)
62+
return Result.new(success?: true) if redirect_to_success_location?(response)
63+
64+
ambiguous_result
65+
end
66+
67+
def reject_result
68+
Result.new(
69+
success?: false,
70+
status: :bad_gateway,
71+
error_code: 'subscription_provider_rejected',
72+
message: 'Subscription provider rejected the request.'
73+
)
74+
end
75+
76+
def ambiguous_result
77+
Result.new(
78+
success?: false,
79+
status: :bad_gateway,
80+
error_code: 'subscription_provider_ambiguous',
81+
message: 'Subscription provider response was ambiguous.'
82+
)
83+
end
84+
85+
def redirect_to_success_location?(response)
86+
location = redirect_location(response)
87+
SUCCESS_LOCATION_PATTERNS.any? { |pattern| location.include?(pattern) }
88+
end
89+
90+
def redirect_to_error_location?(response)
91+
location = redirect_location(response)
92+
ERROR_LOCATION_PATTERNS.any? { |pattern| location.include?(pattern) }
93+
end
94+
95+
def redirect_location(response)
96+
response.headers.fetch('location', '').to_s.downcase
97+
end
98+
99+
def classification_for(response)
100+
return 'rejected_status' unless SUCCESS_STATUS_CODES.include?(response.status)
101+
return 'accepted_200' if response.status == 200
102+
return 'rejected_error_redirect' if redirect_to_error_location?(response)
103+
return 'accepted_success_redirect' if redirect_to_success_location?(response)
104+
105+
'ambiguous_redirect'
106+
end
56107
end
57108
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Subscriptions::PardotFormHandlerSubmitter do
6+
let(:endpoint_url) { 'https://example.com/form-handler' }
7+
let(:service) { described_class.new(endpoint_url: endpoint_url) }
8+
let(:payload) { { 'email' => 'teacher@example.com', 'privacy_policy' => true } }
9+
10+
describe '#call' do
11+
let(:connection) { instance_double(Faraday::Connection) }
12+
let(:response) { instance_double(Faraday::Response, status: status, headers: headers, success?: success) }
13+
let(:headers) { {} }
14+
let(:status) { 200 }
15+
let(:success) { true }
16+
17+
before do
18+
allow(Faraday).to receive(:new).and_return(connection)
19+
allow(connection).to receive(:post).and_return(response)
20+
end
21+
22+
it 'returns success for a 200 response' do
23+
result = service.call(form_payload: payload)
24+
25+
expect(result.success?).to be(true)
26+
end
27+
28+
it 'returns success for a 302 success redirect location' do
29+
allow(response).to receive_messages(status: 302, success?: false, headers: { 'location' => '/subscriptions/success' })
30+
31+
result = service.call(form_payload: payload)
32+
33+
expect(result.success?).to be(true)
34+
end
35+
36+
it 'returns rejected for a 302 error redirect location' do
37+
allow(response).to receive_messages(status: 302, success?: false, headers: { 'location' => '/subscriptions/error' })
38+
39+
result = service.call(form_payload: payload)
40+
41+
aggregate_failures do
42+
expect(result.success?).to be(false)
43+
expect(result.error_code).to eq('subscription_provider_rejected')
44+
end
45+
end
46+
47+
it 'returns ambiguous for an unknown 302 redirect location' do
48+
allow(response).to receive_messages(status: 302, success?: false, headers: { 'location' => '/subscriptions/unknown' })
49+
50+
result = service.call(form_payload: payload)
51+
52+
aggregate_failures do
53+
expect(result.success?).to be(false)
54+
expect(result.error_code).to eq('subscription_provider_ambiguous')
55+
end
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)