-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
103453 - Add validation for VES data - IVC CHAMPVA #21138
Draft
michaelclement
wants to merge
6
commits into
master
Choose a base branch
from
103453-ves-validate-required-fields
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+289
−0
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
14e5622
added initial implementation
michaelclement a4e55c0
linting
michaelclement c5a27b3
cleanup
michaelclement 467de16
added initial specfile for ves data validator
michaelclement a9c85f8
cleanup of specfile
michaelclement ede913a
added another example of acceptable phone num
michaelclement File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
158 changes: 158 additions & 0 deletions
158
modules/ivc_champva/app/services/ivc_champva/ves_data_validator.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# frozen_string_literal: true | ||
|
||
# TODO: add validators for non-required, but structure constrained types: | ||
# - validate phone number structure: ^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$ | ||
# - validate gender values | ||
# - validate relationship to veteran | ||
|
||
module IvcChampva | ||
class VesDataValidator | ||
# This function will run through all the individual validators | ||
def self.validate(request_body) | ||
validate_application_type(request_body) | ||
.then { |rb| validate_application_uuid(rb) } | ||
.then { |rb| validate_sponsor(rb) } | ||
.then { |rb| validate_beneficiaries(rb) } | ||
.then { |rb| validate_certification(rb) } | ||
end | ||
|
||
def self.validate_sponsor(request_body) | ||
sponsor = request_body[:sponsor] | ||
validate_first_name(sponsor) | ||
.then { |s| validate_last_name(s) } | ||
.then { |s| validate_sponsor_address(s) } | ||
.then { |s| validate_date_of_birth(s) } | ||
.then { |s| validate_person_uuid(s) } | ||
.then { |s| validate_ssn(s) } | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_beneficiaries(request_body) | ||
beneficiaries = request_body[:beneficiaries] | ||
raise ArgumentError, 'beneficiaries is invalid. Must be an array' unless beneficiaries.is_a?(Array) | ||
|
||
beneficiaries.each do |beneficiary| | ||
validate_beneficiary(beneficiary) | ||
end | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_beneficiary(beneficiary) | ||
validate_first_name(beneficiary) | ||
.then { |b| validate_last_name(b) } | ||
.then { |b| validate_date_of_birth(b) } | ||
.then { |b| validate_person_uuid(b) } | ||
.then { |b| validate_beneficiary_address(b) } | ||
.then { |b| validate_ssn(b) } | ||
|
||
beneficiary | ||
end | ||
|
||
def self.validate_certification(request_body) | ||
certification = request_body[:certification] | ||
|
||
validate_presence_and_stringiness(certification[:signature], 'certification signature') | ||
validate_date(certification[:signatureDate], 'certification signature date') | ||
|
||
request_body | ||
end | ||
|
||
# ------------------------------------------------------------ # | ||
# ------------------------------------------------------------ # | ||
Comment on lines
+62
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All methods below this line are the individual validation components we roll together up above. There are currently some tests, but the goal is to get much wider coverage of these constituent parts in a follow-on PR. |
||
|
||
def self.validate_application_type(request_body) | ||
validate_presence_and_stringiness(request_body[:applicationType], 'application type') | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_application_uuid(request_body) | ||
validate_presence_and_stringiness(request_body[:applicationUUID], 'application UUID') | ||
validate_uuid_length(request_body[:applicationUUID], 'application UUID') | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_first_name(object) | ||
validate_presence_and_stringiness(object[:firstName], 'first name') | ||
object[:firstName] = transliterate_and_strip(object[:firstName]) | ||
|
||
object | ||
end | ||
|
||
def self.validate_last_name(object) | ||
validate_presence_and_stringiness(object[:lastName], 'last name') | ||
object[:lastName] = transliterate_and_strip(object[:lastName]) | ||
|
||
object | ||
end | ||
|
||
def self.transliterate_and_strip(text) | ||
# Convert any special UTF-8 chars to nearest ASCII equivalents, drop whitespace | ||
I18n.transliterate(text).gsub(%r{[^a-zA-Z\-\/\s]}, '').strip | ||
end | ||
|
||
def self.validate_person_uuid(object) | ||
validate_presence_and_stringiness(object[:personUUID], 'person uuid') | ||
validate_uuid_length(object[:personUUID], 'person uuid') | ||
|
||
object | ||
end | ||
|
||
def self.validate_date_of_birth(object) | ||
validate_date(object[:dateOfBirth], 'date of birth') | ||
|
||
object | ||
end | ||
|
||
def self.validate_beneficiary_address(beneficiary) | ||
validate_address(beneficiary[:address], 'beneficiary') | ||
|
||
beneficiary | ||
end | ||
|
||
def self.validate_sponsor_address(request_body) | ||
validate_address(request_body[:address], 'sponsor') | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_address(address, name) | ||
validate_presence_and_stringiness(address[:city], "#{name} city") | ||
validate_presence_and_stringiness(address[:state], "#{name} state") | ||
validate_presence_and_stringiness(address[:zipCode], "#{name} zip code") | ||
validate_presence_and_stringiness(address[:streetAddress], "#{name} street address") | ||
end | ||
|
||
def self.validate_ssn(request_body) | ||
# TODO: strip out hyphens here? Or do that further up the chain? | ||
validate_presence_and_stringiness(request_body[:ssn], 'ssn') | ||
unless request_body[:ssn].match?(/^(?!(000|666|9))\d{3}(?!00)\d{2}(?!0000)\d{4}$/) | ||
raise ArgumentError, 'ssn is invalid. Must be 9 digits (see regex for more detail)' | ||
end | ||
|
||
request_body | ||
end | ||
|
||
def self.validate_date(date, name) | ||
validate_presence_and_stringiness(date, name) | ||
raise ArgumentError, 'date is invalid. Must match YYYY-MM-DD' unless date.match?(/^\d{4}-\d{2}-\d{2}$/) | ||
|
||
# TODO: once we know the exact date format VES is expecting we can | ||
# do further checks here. | ||
date | ||
end | ||
|
||
def self.validate_uuid_length(uuid, name) | ||
# TODO: we may want a more sophisticated validation for uuids | ||
raise ArgumentError, "#{name} is invalid. Must be 36 characters" unless uuid.length == 36 | ||
end | ||
|
||
def self.validate_presence_and_stringiness(value, error_label) | ||
raise ArgumentError, "#{error_label} is missing" unless value | ||
raise ArgumentError, "#{error_label} is not a string" if value.class != String | ||
end | ||
end | ||
end |
131 changes: 131 additions & 0 deletions
131
modules/ivc_champva/spec/services/ves_data_validator_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
valid_data = { | ||
applicationType: 'CHAMPVA Application', | ||
applicationUUID: '12345678-1234-5678-1234-567812345678', | ||
sponsor: { | ||
personUUID: '52345678-1234-5678-1234-567812345678', | ||
firstName: 'Joe', | ||
lastName: 'Johnson', | ||
middleInitial: 'X', | ||
ssn: '123123123', | ||
vaFileNumber: '', | ||
dateOfBirth: '1999-01-01', | ||
dateOfMarriage: '', | ||
isDeceased: true, | ||
dateOfDeath: '1999-01-01', | ||
isDeathOnActiveService: false, | ||
address: { | ||
streetAddress: '123 Certifier Street ', | ||
city: 'Citytown', | ||
state: 'AL', | ||
zipCode: '12312' | ||
} | ||
}, | ||
beneficiaries: [ | ||
{ | ||
personUUID: '62345678-1234-5678-1234-567812345678', | ||
firstName: 'Johnny', | ||
lastName: 'Alvin', | ||
middleInitial: 'T', | ||
ssn: '345345345', | ||
emailAddress: '[email protected]', | ||
phoneNumber: '+1 (555) 555-1234', | ||
gender: 'MALE', | ||
enrolledInMedicare: true, | ||
hasOtherInsurance: true, | ||
relationshipToSponsor: 'CHILD', | ||
childtype: 'ADOPTED', | ||
address: { | ||
streetAddress: '456 Circle Street ', | ||
city: 'Clinton', | ||
state: 'AS', | ||
zipCode: '56790' | ||
}, | ||
dateOfBirth: '2000-01-01' | ||
} | ||
], | ||
certification: { | ||
signature: 'certifier jones', | ||
signatureDate: '1999-01-01', | ||
firstName: 'Certifier', | ||
lastName: 'Jones', | ||
middleInitial: 'X', | ||
phoneNumber: '(123) 123-1234' | ||
} | ||
} | ||
|
||
describe IvcChampva::VesDataValidator do | ||
describe 'data is valid' do | ||
it 'returns unmodified data' do | ||
# Deep copy our properly formatted data | ||
request_body = Marshal.load(Marshal.dump(valid_data)) | ||
|
||
validated_data = IvcChampva::VesDataValidator.validate(request_body) | ||
|
||
expect(validated_data).to eq(request_body) | ||
end | ||
end | ||
|
||
describe 'request_body key has a missing value' do | ||
it 'raises a missing exception' do | ||
expect do | ||
IvcChampva::VesDataValidator.validate_presence_and_stringiness(nil, 'sponsor first name') | ||
end.to raise_error(ArgumentError, 'sponsor first name is missing') | ||
end | ||
end | ||
|
||
describe 'string key has a non-string value' do | ||
it 'raises a non-string exception' do | ||
expect do | ||
IvcChampva::VesDataValidator.validate_presence_and_stringiness(12, 'sponsor first name') | ||
end.to raise_error(ArgumentError, 'sponsor first name is not a string') | ||
end | ||
end | ||
|
||
describe 'sponsor first name is malformed' do | ||
describe 'contains disallowed characters' do | ||
it 'returns data with disallowed characters of sponsor first name stripped or corrected' do | ||
# Deep copy our valid data | ||
request_body = Marshal.load(Marshal.dump(valid_data)) | ||
request_body[:sponsor][:firstName] = '2Jöhn~! - Jo/hn?\\' | ||
expected_sponsor_name = 'John - Jo/hn' | ||
|
||
validated_data = IvcChampva::VesDataValidator.validate(request_body) | ||
|
||
expect(validated_data[:sponsor][:firstName]).to eq expected_sponsor_name | ||
end | ||
end | ||
end | ||
|
||
describe 'sponsor last name is malformed' do | ||
describe 'contains disallowed characters' do | ||
it 'returns data with disallowed characters of sponsor last name stripped or corrected' do | ||
# Deep copy our valid data | ||
request_body = Marshal.load(Marshal.dump(valid_data)) | ||
request_body[:sponsor][:lastName] = '2Jöhnşon~!\\' | ||
expected_sponsor_name = 'Johnson' | ||
|
||
validated_data = IvcChampva::VesDataValidator.validate(request_body) | ||
|
||
expect(validated_data[:sponsor][:lastName]).to eq expected_sponsor_name | ||
end | ||
end | ||
end | ||
|
||
describe 'social security number is malformed' do | ||
describe 'too long' do | ||
it 'raises an exception' do | ||
# Deep copy our valid data | ||
request_body = Marshal.load(Marshal.dump(valid_data)) | ||
request_body[:sponsor][:ssn] = '1234567890' | ||
|
||
expect do | ||
IvcChampva::VesDataValidator.validate(request_body) | ||
end.to raise_error(ArgumentError, 'ssn is invalid. Must be 9 digits (see regex for more detail)') | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outside the scope of this ticket, but there will be a followup