Skip to content

Commit

Permalink
- sample Million Verifier service
Browse files Browse the repository at this point in the history
- try email verifier in Business Contact Info
- added/updated unit tests
  • Loading branch information
severinbeauvais committed Oct 20, 2022
1 parent c65ed59 commit 1c94c42
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 3 deletions.
7 changes: 6 additions & 1 deletion src/components/common/YourCompany/BusinessContactInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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, MillionVerifierService } from '@/services/'
import { CommonMixin } from '@/mixins/'
import { ActionBindingIF, ResourceIF, EntitySnapshotIF } from '@/interfaces/'
import { ContactPointIF } from '@bcrs-shared-components/interfaces/'
Expand Down Expand Up @@ -69,6 +69,11 @@ export default class BusinessContactInfo extends Mixins(CommonMixin) {
this.isCorrectionFiling ||
this.isSpecialResolutionFiling
) {
const valid = await MillionVerifierService.isValidEmail(contactInfo.email)
if (!valid) {
alert('Invalid email address')
return
}
await AuthServices.updateContactInfo(contactInfo, this.getBusinessId)
}
this.setBusinessContact(contactInfo)
Expand Down
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as AuthServices } from './auth-services'
export { default as BusinessLookupServices } from './business-lookup-services'
export { default as LegalServices } from './legal-services'
export { default as MillionVerifierService } from './million-verifier-service'
export { default as NaicsServices } from './naics-services'
53 changes: 53 additions & 0 deletions src/services/million-verifier-service.ts
Original file line number Diff line number Diff line change
@@ -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 MillionVerifierService {
/**
* 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<boolean> {
// 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)
})
}
}
8 changes: 6 additions & 2 deletions tests/unit/BusinessContactInfo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { getVuexStore } from '@/store/'
import { mount } from '@vue/test-utils'
import BusinessContactInfo from '@/components/common/YourCompany/BusinessContactInfo.vue'
import AuthServices from '@/services/auth-services'
import MillionVerifierService from '@/services/million-verifier-service'

Vue.use(Vuetify)

const vuetify = new Vuetify({})
const store = getVuexStore()

// mock services function
const mockUpdateContactInfo = jest.spyOn((AuthServices as any), 'updateContactInfo').mockImplementation()
// mock auth services function
jest.spyOn((AuthServices as any), 'updateContactInfo').mockImplementation()

// mock million verifier service function
jest.spyOn((MillionVerifierService as any), 'isValidEmail').mockReturnValue(true)

const contactInfo = {
email: '[email protected]',
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/million-verifier-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import sinon from 'sinon'
import axios from 'axios'
import MillionVerifierService from '@/services/million-verifier-service'

describe('Million Verifier 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(MillionVerifierService.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(MillionVerifierService.isValidEmail('[email protected]', null, 'KEY', 5))
.rejects.toThrow('API URL is required')
})

it('throws an error when there is no timeout', async () => {
// test it
await expect(MillionVerifierService.isValidEmail('[email protected]', 'https://url', 'KEY', null))
.rejects.toThrow('Timeout is required')
})

it('returns True when there is no API key', async () => {
// test it
expect(await MillionVerifierService.isValidEmail('[email protected]', '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 MillionVerifierService.isValidEmail('[email protected]', '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 MillionVerifierService.isValidEmail('[email protected]', '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 MillionVerifierService.isValidEmail('[email protected]', '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(MillionVerifierService.isValidEmail('[email protected]', '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(MillionVerifierService.isValidEmail('[email protected]', 'https://url', 'KEY', 5))
.rejects.toThrow('Network error')
})
})

0 comments on commit 1c94c42

Please sign in to comment.