Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/controllers/idv/in_person/usps_locations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

module Idv
module InPerson
class UspsLocationsError < StandardError
def initialize
super('Unsupported characters in address field.')
end
end

class UspsLocationsController < ApplicationController
include Idv::AvailabilityConcern
include Idv::HybridMobile::HybridMobileConcern
Expand All @@ -18,6 +24,7 @@ class UspsLocationsController < ApplicationController
rescue_from ActionController::InvalidAuthenticityToken,
Faraday::Error,
StandardError,
UspsLocationsError,
Faraday::BadRequestError,
with: :handle_error

Expand All @@ -28,6 +35,11 @@ def index
city: search_params['city'], state: search_params['state'],
zip_code: search_params['zip_code']
)

unless candidate.has_valid_address?
raise UspsLocationsError.new
end

locations = proofer.request_facilities(candidate, authn_context_enhanced_ipp?)
if locations.length > 0
analytics.idv_in_person_locations_searched(
Expand Down Expand Up @@ -96,7 +108,8 @@ def localized_locations(locations)
def handle_error(err)
remapped_error = case err
when ActionController::InvalidAuthenticityToken,
Faraday::Error
Faraday::Error,
UspsLocationsError
:unprocessable_entity
else
:internal_server_error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { RegisterFieldCallback } from '@18f/identity-form-steps';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { SpinnerButtonRefHandle, SpinnerButton } from '@18f/identity-spinner-button';
import { ValidatedField } from '@18f/identity-validated-field';
import { t } from '@18f/identity-i18n';
import { useI18n } from '@18f/identity-react-i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import useValidatedUspsLocations from '../hooks/use-validated-usps-locations';

Expand All @@ -30,6 +30,7 @@ export default function FullAddressSearchInput({
registerField = () => undefined,
usStatesTerritories,
}: FullAddressSearchInputProps) {
const { t } = useI18n();
const spinnerButtonRef = useRef<SpinnerButtonRefHandle>(null);
const [addressValue, setAddressValue] = useState('');
const [cityValue, setCityValue] = useState('');
Expand Down Expand Up @@ -82,12 +83,27 @@ export default function FullAddressSearchInput({
[addressValue, cityValue, stateValue, zipCodeValue],
);

const getErroneousAddressChars = () => {
const addressReStr = validatedAddressFieldRef.current?.pattern;

if (!addressReStr) {
return;
}

const addressRegex = new RegExp(addressReStr, 'g');
const errChars = addressValue.replace(addressRegex, '');
const uniqErrChars = [...new Set(errChars.split(''))].join('');
return uniqErrChars;
};

return (
<>
<ValidatedField
ref={validatedAddressFieldRef}
messages={{
patternMismatch: t('simple_form.required.text'),
patternMismatch: t('in_person_proofing.form.address.errors.unsupported_chars', {
char_list: getErroneousAddressChars(),
}),
}}
>
<TextInput
Expand All @@ -98,7 +114,7 @@ export default function FullAddressSearchInput({
label={t('in_person_proofing.body.location.po_search.address_label')}
disabled={disabled}
maxLength={255}
pattern=".*\S.*$"
pattern="[A-Za-z0-9\-' .\/#]*"
/>
</ValidatedField>
<ValidatedField
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import type { SetupServer } from 'msw/node';
import { SWRConfig } from 'swr';
import { I18n } from '@18f/identity-i18n';
import { I18nContext } from '@18f/identity-react-i18n';
import FullAddressSearch from './full-address-search';

describe('FullAddressSearch', () => {
Expand Down Expand Up @@ -133,6 +135,62 @@ describe('FullAddressSearch', () => {
expect(errors).to.have.lengthOf(4);
});

it('displays an error for unsupported characters in address field', async () => {
const handleLocationsFound = sandbox.stub();
const locationCache = new Map();
const { findByText, findByLabelText } = render(
<I18nContext.Provider
value={
new I18n({
strings: {
'in_person_proofing.form.address.errors.unsupported_chars':
'Our system cannot read the following characters: %{char_list} . Please try again using substitutes for those characters.',
},
})
}
>
<SWRConfig value={{ provider: () => locationCache }}>
<FullAddressSearch
usStatesTerritories={usStatesTerritories}
onFoundLocations={handleLocationsFound}
locationsURL={locationsURL}
registerField={() => undefined}
handleLocationSelect={undefined}
disabled={false}
/>
</SWRConfig>
,
</I18nContext.Provider>,
);

await userEvent.type(
await findByLabelText('in_person_proofing.body.location.po_search.address_label'),
'20, main',
);
await userEvent.type(
await findByLabelText('in_person_proofing.body.location.po_search.city_label'),
'Endeavor',
);
await userEvent.selectOptions(
await findByLabelText('in_person_proofing.body.location.po_search.state_label'),
'DE',
);
await userEvent.type(
await findByLabelText('in_person_proofing.body.location.po_search.zipcode_label'),
'00010',
);
await userEvent.click(
await findByText('in_person_proofing.body.location.po_search.search_button'),
);

const error = await findByText(
'Our system cannot read the following characters: , . Please try again using substitutes for those characters.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified text here to prove test was working properly. Good

);

expect(error).to.exist();
expect(locationCache.size).to.equal(1);
});

it('displays an error for an invalid ZIP code length (length = 1)', async () => {
const handleLocationsFound = sandbox.stub();
const { findByText, findByLabelText, findAllByText } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { t } from '@18f/identity-i18n';

export default function useValidatedUspsLocations(locationsURL: string) {
const [locationQuery, setLocationQuery] = useState<LocationQuery | null>(null);
const validatedAddressFieldRef = useRef<HTMLFormElement>(null);
const validatedCityFieldRef = useRef<HTMLFormElement>(null);
const validatedStateFieldRef = useRef<HTMLFormElement>(null);
const validatedZipCodeFieldRef = useRef<HTMLFormElement>(null);
const validatedAddressFieldRef = useRef<HTMLInputElement>(null);
const validatedCityFieldRef = useRef<HTMLInputElement>(null);
const validatedStateFieldRef = useRef<HTMLInputElement>(null);
const validatedZipCodeFieldRef = useRef<HTMLInputElement>(null);

const checkValidityAndDisplayErrors = (address, city, state, zipCode) => {
let formIsValid = true;
Expand Down Expand Up @@ -48,7 +48,14 @@ export default function useValidatedUspsLocations(locationsURL: string) {
validatedStateFieldRef.current?.reportValidity();
validatedZipCodeFieldRef.current?.reportValidity();

return formIsValid && zipCodeIsValid;
const hasInvalidFields = [
validatedAddressFieldRef,
validatedCityFieldRef,
validatedStateFieldRef,
validatedZipCodeFieldRef,
].some((fieldRef) => fieldRef.current?.validity?.valid === false);

return formIsValid && zipCodeIsValid && !hasInvalidFields;
};

const handleLocationSearch = useCallback(
Expand Down
6 changes: 5 additions & 1 deletion app/services/usps_in_person_proofing/applicant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ module UspsInPersonProofing
Applicant = Struct.new(
:unique_id, :first_name, :last_name, :address, :city, :state, :zip_code,
:email, keyword_init: true
)
) do
def has_valid_address?
(address =~ /[^A-Za-z0-9\-' .\/#]/).nil?
end
end
end
15 changes: 15 additions & 0 deletions spec/controllers/idv/in_person/usps_locations_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@
end
end

context 'address has unsupported characters' do
subject(:response) do
post :index, params: { locale: locale,
address: { street_address: '1600, Pennsylvania Ave',
city: 'Washington',
state: 'DC',
zip_code: '20500' } }
end

it 'returns unprocessable entity' do
subject
expect(response.status).to eq 422
end
end

context 'no addresses found by usps' do
before do
allow(proofer).to receive(:request_facilities).with(address, false).
Expand Down