Skip to content

Commit

Permalink
[Automated] Merged master into target preview_envs_k8s
Browse files Browse the repository at this point in the history
  • Loading branch information
va-vsp-bot authored Mar 25, 2024
2 parents 68e618e + eb77987 commit fa606a6
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 0 deletions.
73 changes: 73 additions & 0 deletions app/controllers/v1/pension_ipf_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

require 'decision_review_v1/utilities/logging_utils'

module V1
class PensionIpfCallbacksController < ApplicationController
include ActionController::HttpAuthentication::Token::ControllerMethods
include DecisionReviewV1::Appeals::LoggingUtils

service_tag 'pension-ipf-callbacks'

skip_before_action :verify_authenticity_token, only: [:create]
skip_before_action :authenticate, only: [:create]
skip_after_action :set_csrf_header, only: [:create]
before_action :authenticate_header, only: [:create]

STATUSES_TO_IGNORE = %w[sent delivered temporary-failure].freeze

def create
return render json: nil, status: :not_found unless Flipper.enabled? :pension_ipf_callbacks_endpoint

payload = JSON.parse(request.body.string)

# save encrypted request body in database table for non-successful notifications
payload_status = payload['status']&.downcase
if STATUSES_TO_IGNORE.exclude? payload_status
begin
PensionIpfNotification.create!(payload:)
rescue ActiveRecord::RecordInvalid => e
log_formatted(**log_params(payload).merge(is_success: false), params: { exception_message: e.message })
return render json: { message: 'failed' }
end
end

log_formatted(**log_params(payload).merge(is_success: true))
render json: { message: 'success' }
end

private

def authenticate_header
authenticate_user_with_token || authenticity_error
end

def authenticate_user_with_token
Rails.logger.info('pension-ipf-callbacks-69766 - Received request, authenticating')
authenticate_with_http_token do |token|
return false if bearer_token_secret.nil?

token == bearer_token_secret
end
end

def authenticity_error
Rails.logger.info('pension-ipf-callbacks-69766 - Failed to authenticate request')
render json: { message: 'Invalid credentials' }, status: :unauthorized
end

def bearer_token_secret
Settings.dig(:pension_ipf_vanotify_status_callback, :bearer_token)
end

def log_params(payload)
{
key: :callbacks,
form_id: '21P-527EZ',
user_uuid: nil,
upstream_system: 'VANotify',
body: payload.merge('to' => '<FILTERED>') # scrub PII from logs
}
end
end
end
20 changes: 20 additions & 0 deletions app/models/pension_ipf_notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require 'json_marshal/marshaller'

class PensionIpfNotification < ApplicationRecord
serialize :payload, JsonMarshal::Marshaller

has_kms_key
has_encrypted :payload, key: :kms_key, **lockbox_options

validates(:payload, presence: true)

before_save :serialize_payload

private

def serialize_payload
self.payload = payload.to_json unless payload.is_a?(String)
end
end
4 changes: 4 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,10 @@ features:
actor_type: user
description: NOD VANotify notification callbacks endpoint
enable_in_development: true
pension_ipf_callbacks_endpoint:
actor_type: user
description: Pension IPF VANotify notification callbacks endpoint
enable_in_development: true
hlr_browser_monitoring_enabled:
actor_type: user
description: HLR Datadog RUM monitoring
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@

scope format: false do
resources :nod_callbacks, only: [:create]
resources :pension_ipf_callbacks, only: [:create]
end
end

Expand Down
3 changes: 3 additions & 0 deletions config/settings/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,6 @@ travel_pay:
subscription_key: 'api_key'
base_url: 'https://btsss.gov'
service_name: 'BTSSS-API'

pension_ipf_vanotify_status_callback:
bearer_token: bearer_token_secret
90 changes: 90 additions & 0 deletions spec/controllers/v1/pension_ipf_callbacks_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe V1::PensionIpfCallbacksController, type: :controller do
let(:status) { 'delivered' }
let(:params) do
{
id: '6ba01111-f3ee-4a40-9d04-234asdfb6abab9c',
reference: nil,
to: '[email protected]',
status:,
created_at: '2023-01-10T00:04:25.273410Z',
completed_at: '2023-01-10T00:05:33.255911Z',
sent_at: '2023-01-10T00:04:25.775363Z',
notification_type: 'email',
status_reason: '',
provider: 'sendgrid'
}
end

describe '#create' do
before do
request.headers['Authorization'] = "Bearer #{Settings.pension_ipf_vanotify_status_callback.bearer_token}"
Flipper.enable(:pension_ipf_callbacks_endpoint)
allow(PensionIpfNotification).to receive(:create!)
end

context 'with payload' do
context 'if status is delivered' do
it 'returns success and does not save a record of the payload' do
post(:create, params:, as: :json)

expect(PensionIpfNotification).not_to receive(:create!)

expect(response).to have_http_status(:ok)

res = JSON.parse(response.body)
expect(res['message']).to eq 'success'
end
end

context 'if status is a failure that will not retry' do
let(:status) { 'permanent-failure' }

it 'returns success' do
post(:create, params:, as: :json)

expect(response).to have_http_status(:ok)

res = JSON.parse(response.body)
expect(res['message']).to eq 'success'
end

context 'and the record failed to save' do
before do
allow(PensionIpfNotification).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
end

it 'returns failed' do
post(:create, params:, as: :json)

expect(response).to have_http_status(:ok)

res = JSON.parse(response.body)
expect(res['message']).to eq 'failed'
end
end
end
end
end

describe 'authentication' do
context 'with missing Authorization header' do
it 'returns 401' do
request.headers['Authorization'] = nil
post(:create, params:, as: :json)
expect(response).to have_http_status(:unauthorized)
end
end

context 'with invalid Authorization header' do
it 'returns 401' do
request.headers['Authorization'] = 'Bearer foo'
post(:create, params:, as: :json)
expect(response).to have_http_status(:unauthorized)
end
end
end
end
20 changes: 20 additions & 0 deletions spec/factories/pension_ipf_notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

FactoryBot.define do
factory :pension_ipf_notification do
payload do
{
id: '6ba01111-f3ee-4a40-9d04-234asdfb6abab9c',
reference: nil,
to: '[email protected]',
status: 'delivered',
created_at: '2023-01-10T00:04:25.273410Z',
completed_at: '2023-01-10T00:05:33.255911Z',
sent_at: '2023-01-10T00:04:25.775363Z',
notification_type: 'email',
status_reason: '',
provider: 'pinpoint'
}
end
end
end
34 changes: 34 additions & 0 deletions spec/models/pension_ipf_notification_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PensionIpfNotification, type: :model do
let(:pension_ipf_notification) { build(:pension_ipf_notification) }

describe 'payload encryption' do
it 'encrypts the payload field' do
expect(subject).to encrypt_attr(:payload)
end
end

describe 'validations' do
it 'validates presence of payload' do
expect_attr_valid(pension_ipf_notification, :payload)
pension_ipf_notification.payload = nil
expect_attr_invalid(pension_ipf_notification, :payload, "can't be blank")
end
end

describe '#serialize_payload' do
let(:payload) do
{ a: 1 }
end

it 'serializes payload as json' do
pension_ipf_notification.payload = payload
pension_ipf_notification.save!

expect(pension_ipf_notification.payload).to eq(payload.to_json)
end
end
end

0 comments on commit fa606a6

Please sign in to comment.