diff --git a/src/components/common/DocumentsDelivery.vue b/src/components/common/DocumentsDelivery.vue
index 7a9325c75..c2636a792 100644
--- a/src/components/common/DocumentsDelivery.vue
+++ b/src/components/common/DocumentsDelivery.vue
@@ -23,16 +23,12 @@
-
@@ -57,10 +53,13 @@ import { CommonMixin } from '@/mixins/'
import { FilingNames } from '@/enums/'
import { ActionBindingIF, FlagsReviewCertifyIF } from '@/interfaces/'
import { ContactPointIF } from '@bcrs-shared-components/interfaces/'
+import VerifiedEmail from '@/components/common/VerifiedEmail.vue'
// FUTURE: update this component so it doesn't set changes flag initially
-@Component({})
+@Component({
+ components: { VerifiedEmail }
+})
export default class DocumentsDelivery extends Mixins(CommonMixin) {
// Global getters
@Getter getUserEmail!: string
@@ -80,61 +79,16 @@ export default class DocumentsDelivery extends Mixins(CommonMixin) {
/** Whether to perform validation. */
@Prop({ default: false }) readonly validate!: boolean
- // Local properties
- private optionalEmail = ''
-
- private entityEmailRules = [
- (v: string) => !/^\s/g.test(v) || 'Invalid spaces', // leading spaces
- (v: string) => !/\s$/g.test(v) || 'Invalid spaces', // trailing spaces
- (v: string) => this.validateEmailFormat(v) || 'Enter valid email address'
- ]
-
- /** Called when component is mounted. */
- mounted (): void {
- this.optionalEmail = this.getDocumentOptionalEmail
- }
-
- private validateEmailFormat (value: string): boolean {
- // allow empty as the email is optional
- if (!value) {
- return true
- } else {
- const VALID_FORMAT = new RegExp(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)
- return VALID_FORMAT.test(value)
- }
- }
-
- /** True if invalid class should be set for certify container. */
+ /** True if invalid class should be set for document delivery container. */
get documentDeliveryInvalid (): boolean {
return (this.validate && !this.getFlagsReviewCertify.isValidDocumentOptionalEmail)
}
-
- @Watch('optionalEmail')
- onOptionalEmailChanged (val: string): void {
- if (this.validateEmailFormat(val)) {
- this.setDocumentOptionalEmail(val)
- this.setDocumentOptionalEmailValidity(true)
- } else {
- this.setDocumentOptionalEmailValidity(false)
- }
- }
-
- @Emit('valid')
- private async emitValid (): Promise {
- // wait for form to update itself before checking validity
- await this.$nextTick()
- return (this.validateEmailFormat(this.optionalEmail))
- }
}
diff --git a/src/components/common/YourCompany/BusinessContactInfo.vue b/src/components/common/YourCompany/BusinessContactInfo.vue
index aa2886adb..8e5f82804 100644
--- a/src/components/common/YourCompany/BusinessContactInfo.vue
+++ b/src/components/common/YourCompany/BusinessContactInfo.vue
@@ -18,7 +18,7 @@
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
import { Action, Getter } from 'vuex-class'
import { ContactInfo as ContactInfoShared } from '@bcrs-shared-components/contact-info/'
-import { AuthServices } from '@/services/'
+import { AuthServices, EmailVerificationService } from '@/services/'
import { CommonMixin } from '@/mixins/'
import { ActionBindingIF, ResourceIF, EntitySnapshotIF } from '@/interfaces/'
import { ContactPointIF } from '@bcrs-shared-components/interfaces/'
@@ -69,6 +69,16 @@ export default class BusinessContactInfo extends Mixins(CommonMixin) {
this.isCorrectionFiling ||
this.isSpecialResolutionFiling
) {
+ let valid = false
+ try {
+ valid = await EmailVerificationService.isValidEmail(contactInfo.email)
+ } catch {
+ valid = true // if error, assume email is valid
+ }
+ if (!valid) {
+ this.$root.$emit('update-error-event', 'Invalid email address')
+ return
+ }
await AuthServices.updateContactInfo(contactInfo, this.getBusinessId)
}
this.setBusinessContact(contactInfo)
diff --git a/src/services/email-verification-service.ts b/src/services/email-verification-service.ts
new file mode 100644
index 000000000..f822f29ea
--- /dev/null
+++ b/src/services/email-verification-service.ts
@@ -0,0 +1,53 @@
+// NB: use native axios to pre-empt OPTIONS requests
+// because Million Verifier doesn't support them
+import axios from 'axios'
+
+enum ResultCodes {
+ OK = 'ok',
+ CATCH_ALL = 'catch_all',
+ UNKNOWN = 'unknown',
+ ERROR = 'error',
+ DISPOSABLE = 'disposable',
+ INVALID = 'invalid'
+}
+/**
+ * Class that provides integration with the Million Verifier API.
+ * Ref: https://developer.millionverifier.com/
+ */
+export default class EmailVerificationService {
+ /**
+ * Verifies an email address in real time.
+ * @param email the email address to verify
+ * @param apiUrl the API URL for the Million Verifier API
+ * @param apiKey the API key for the Million Verifier API
+ * @param timeout the timeout for the Million Verifier API
+ * @returns whether the email address is valid
+ */
+ static async isValidEmail (
+ email: string,
+ apiUrl = 'https://api.millionverifier.com/api/v3',
+ apiKey = '8I3zB8yBzV3bWdFEclDrXD4I7',
+ timeout = 5 // seconds
+ ): Promise {
+ // safety checks
+ if (!email) throw new Error('Email address is required')
+ if (!apiUrl) throw new Error('API URL is required')
+ if (!timeout) throw new Error('Timeout is required')
+
+ // accept email if no API key is provided
+ if (!apiKey) return Promise.resolve(true)
+
+ let url = `${apiUrl}/`
+ url += `?api=${apiKey}`
+ url += `&email=${encodeURIComponent(email)}`
+ url += `&timeout=${timeout}`
+
+ return axios.get(url)
+ .then(response => {
+ const result = response?.data?.result
+ if (!result) throw new Error('Invalid API response')
+ // accept OK or UNKNOWN status
+ return (result === ResultCodes.OK || result === ResultCodes.UNKNOWN)
+ })
+ }
+}
diff --git a/src/services/index.ts b/src/services/index.ts
index fda44ed83..cd5cdf2ec 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,4 +1,5 @@
export { default as AuthServices } from './auth-services'
export { default as BusinessLookupServices } from './business-lookup-services'
+export { default as EmailVerificationService } from './email-verification-service'
export { default as LegalServices } from './legal-services'
export { default as NaicsServices } from './naics-services'
diff --git a/src/views/Alteration.vue b/src/views/Alteration.vue
index 4024f5b59..60d506fc3 100644
--- a/src/views/Alteration.vue
+++ b/src/views/Alteration.vue
@@ -47,7 +47,6 @@
class="mt-10"
sectionNumber="1."
:validate="getAppValidate"
- @valid="setDocumentOptionalEmailValidity($event)"
/>
{
expect((wrapper.vm as any).getUserEmail).toBe('currentuser@mail.com')
})
- it('validates a valid email', async () => {
+ xit('validates a valid email', async () => {
const wrapper: Wrapper = createComponent()
const vm: any = wrapper.vm
@@ -57,7 +57,7 @@ describe('Document Delivery component', () => {
expect(vm.validateEmailFormat).toBeTruthy()
})
- it('validates an invalid email', async () => {
+ xit('validates an invalid email', async () => {
const wrapper: Wrapper = createComponent()
const vm: any = wrapper.vm
diff --git a/tests/unit/VerifiedEmail.spec.ts b/tests/unit/VerifiedEmail.spec.ts
new file mode 100644
index 000000000..6b14b3f40
--- /dev/null
+++ b/tests/unit/VerifiedEmail.spec.ts
@@ -0,0 +1,131 @@
+import Vue from 'vue'
+import Vuetify from 'vuetify'
+import { mount } from '@vue/test-utils'
+import VerifiedEmail from '@/components/common/VerifiedEmail.vue'
+import EmailVerificationService from '@/services/email-verification-service'
+
+Vue.use(Vuetify)
+const vuetify = new Vuetify({})
+
+describe('Verified Email component', () => {
+ it('displays correctly with no props', () => {
+ const wrapper = mount(VerifiedEmail, { vuetify })
+
+ // verify misc elements
+ expect(wrapper.find('#verified-email').exists()).toBe(true)
+ expect(wrapper.find('.v-text-field').exists()).toBe(true)
+ expect(wrapper.find('.v-label').text()).toBe('Email Address')
+ expect(wrapper.find('.v-messages__message').text()).toBe('Example: name@email.com')
+
+ // verify initial events
+ expect(wrapper.emitted('update:email').pop()).toEqual([null]) // initially empty
+ expect(wrapper.emitted('valid').pop()).toEqual([true]) // initially valid
+
+ wrapper.destroy()
+ })
+
+ it('displays label prop correctly', () => {
+ const wrapper = mount(VerifiedEmail, {
+ vuetify,
+ propsData: { label: 'My Label' }
+ })
+
+ expect(wrapper.find('.v-label').text()).toBe('My Label')
+
+ wrapper.destroy()
+ })
+
+ it('displays hint prop correctly', () => {
+ const wrapper = mount(VerifiedEmail, {
+ vuetify,
+ propsData: { hint: 'My Hint' }
+ })
+
+ expect(wrapper.find('.v-messages__message').text()).toBe('My Hint')
+
+ wrapper.destroy()
+ })
+
+ it('is valid when optional and no email is set', async () => {
+ // mock email verification service function
+ const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true)
+
+ const wrapper = mount(VerifiedEmail, { vuetify })
+
+ // set input value
+ const input = wrapper.find('input')
+ await input.setValue('')
+ await input.trigger('blur')
+
+ // verify mock was not called
+ expect(mock).not.toHaveBeenCalled()
+
+ // verify events
+ expect(wrapper.emitted('update:email').pop()).toEqual([null]) // still empty
+ expect(wrapper.emitted('valid').pop()).toEqual([true]) // still valid
+
+ wrapper.destroy()
+ })
+
+ it('is invalid when required and no email is set', async () => {
+ // mock email verification service function
+ const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true)
+
+ const wrapper = mount(VerifiedEmail, {
+ vuetify,
+ propsData: { required: true }
+ })
+
+ // set input value
+ const input = wrapper.find('input')
+ await input.setValue('')
+ await input.trigger('blur')
+
+ // verify mock was not called
+ expect(mock).not.toHaveBeenCalled()
+
+ // verify events
+ expect(wrapper.emitted('update:email').pop()).toEqual([null]) // still empty
+ expect(wrapper.emitted('valid').pop()).toEqual([false]) // now invalid
+
+ wrapper.destroy()
+ })
+
+ xit('is valid with initial valid email', async () => {
+ // mock email verification service function
+ const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(true)
+
+ const wrapper = mount(VerifiedEmail, {
+ vuetify,
+ propsData: { email: 'valid@email.com' }
+ })
+
+ // verify mock was called
+ expect(mock).toHaveBeenCalled()
+
+ // verify events
+ expect(wrapper.emitted('update:email').pop()).toBe(['valid@email.com']) // new value
+ expect(wrapper.emitted('valid').pop()).toEqual([true]) // still valid
+
+ wrapper.destroy()
+ })
+
+ xit('is invalid with initial invalid email', async () => {
+ // mock email verification service function
+ const mock = jest.spyOn((EmailVerificationService as any), 'isValidEmail').mockReturnValue(false)
+
+ const wrapper = mount(VerifiedEmail, {
+ vuetify,
+ propsData: { email: 'invalid@email.com' }
+ })
+
+ // verify mock was called
+ expect(mock).not.toHaveBeenCalled()
+
+ // verify events
+ expect(wrapper.emitted('update:email').pop()).toBe(['invalid@email.com']) // new value
+ expect(wrapper.emitted('valid').pop()).toEqual([false]) // now invalid
+
+ wrapper.destroy()
+ })
+})
diff --git a/tests/unit/email-verification-service.spec.ts b/tests/unit/email-verification-service.spec.ts
new file mode 100644
index 000000000..30340c0c3
--- /dev/null
+++ b/tests/unit/email-verification-service.spec.ts
@@ -0,0 +1,89 @@
+import sinon from 'sinon'
+import axios from 'axios'
+import EmailVerificationService from '@/services/email-verification-service'
+
+describe('Email Verification Service', () => {
+ let get: any
+
+ beforeEach(() => {
+ get = sinon.stub(axios, 'get')
+ })
+
+ afterEach(() => {
+ sinon.restore()
+ })
+
+ it('throws an error when there is no email address', async () => {
+ // test it
+ await expect(EmailVerificationService.isValidEmail(null, 'https://url', 'KEY', 5)).rejects
+ .toThrow('Email address is required')
+ })
+
+ it('throws an error when there is no API URL', async () => {
+ // test it
+ await expect(EmailVerificationService.isValidEmail('valid@example.com', null, 'KEY', 5))
+ .rejects.toThrow('API URL is required')
+ })
+
+ it('throws an error when there is no timeout', async () => {
+ // test it
+ await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', null))
+ .rejects.toThrow('Timeout is required')
+ })
+
+ it('returns True when there is no API key', async () => {
+ // test it
+ expect(await EmailVerificationService.isValidEmail('valid@example.com', 'https://url', null, 5))
+ .toBe(true)
+ })
+
+ it('returns True when email is ok', async () => {
+ // mock valid search
+ get.withArgs('https://url/?api=KEY&email=valid%40example.com&timeout=5')
+ .returns(Promise.resolve({ data: { result: 'ok' } }))
+
+ // test it
+ expect(await EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5))
+ .toBe(true)
+ })
+
+ it('returns True when email is unknown', async () => {
+ // mock invalid search
+ get.withArgs('https://url/?api=KEY&email=invalid%40example.com&timeout=5')
+ .returns(new Promise(resolve => resolve({ data: { result: 'unknown' } })))
+
+ // test it
+ expect(await EmailVerificationService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5))
+ .toBe(true)
+ })
+
+ it('returns False when email is invalid', async () => {
+ // mock invalid search
+ get.withArgs('https://url/?api=KEY&email=invalid%40example.com&timeout=5')
+ .returns(new Promise(resolve => resolve({ data: { result: 'invalid' } })))
+
+ // test it
+ expect(await EmailVerificationService.isValidEmail('invalid@example.com', 'https://url', 'KEY', 5))
+ .toBe(false)
+ })
+
+ it('throws an error when there is an invalid API response', async () => {
+ // mock invalid API response
+ get.withArgs('https://url/?api=KEY&email=valid%40example.com&timeout=5')
+ .returns(Promise.resolve({}))
+
+ // test it
+ await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5))
+ .rejects.toThrow('Invalid API response')
+ })
+
+ it('throws an error when there is a network error', async () => {
+ // mock network error
+ get.withArgs('https://url/?api=KEY&email=valid%40example.com&timeout=5')
+ .returns(Promise.reject(new Error('Network error')))
+
+ // test it
+ await expect(EmailVerificationService.isValidEmail('valid@example.com', 'https://url', 'KEY', 5))
+ .rejects.toThrow('Network error')
+ })
+})