import { Test, TestingModule } from '@nestjs/testing' import { datatype, internet, random } from 'faker' import * as fetchMock from 'fetch-mock-jest' import { sample } from 'lodash' import { factory } from '../../../test/factories' import { Secret } from '../../typings/global' import { HttpService } from '../http' import { Person } from './models' import { SmartfitApi } from './smartfit.api' import { ForeignIntegrationStatus } from './types' jest.mock('node-fetch', () => (globalThis.fetchMock = fetchMock.sandbox())) describe('SmartfitApi', () => { const singleAccessToken = { token: datatype.uuid(), } const myFetchMock = globalThis.fetchMock let api: SmartfitApi let headers: any function endpoint(path: string, restricted = false) { let basePath = process.env.SMARTFIT_API_ENDPOINT if (restricted) basePath = basePath.replace('public/v1', 'restricted/v1') return `${basePath}${path}` } function mockTokenEndpoint(response: any) { myFetchMock.postOnce(endpoint('person_session'), response, { headers: headers }) } beforeAll(async () => { process.env.SMARTFIT_API_ENDPOINT = 'http://smartfit.com/api/public/v1/' process.env.SMARTFIT_API_CLIENT_TOKEN = datatype.uuid() process.env.SMARTFIT_API_AUTHORIZATION_TOKEN = datatype.uuid() headers = { 'Cache-Control': 'no-cache', Authorization: `Token token=${process.env.SMARTFIT_API_AUTHORIZATION_TOKEN}`, 'Content-Type': 'application/json', } globalThis.secret = { smartfit: { privated: { endpoint: 'http://smartfit.com/api/restricted/v1/', token: { authorization: internet.password(), client: null }, }, }, } as Secret const app: TestingModule = await Test.createTestingModule({ providers: [HttpService, SmartfitApi], }).compile() api = app.get(SmartfitApi) myFetchMock.config.overwriteRoutes = true }) afterEach(() => { myFetchMock.mockReset() }) describe('#getPersonByToken', () => { function mockPersonToken(accessToken: any, response: any) { myFetchMock.get(endpoint(`person_session/${accessToken}`), response, { headers }) } describe('when Single Access Token is valid', () => { it('requests and returns the SmartFit student`s data by Single Access Token', async () => { const personData = await factory.build('smartfit:personData', { main_purchase_expired_at: new Date().toString(), }) const person = Person.build(personData) mockPersonToken(singleAccessToken.token, personData) expect(await api.getPersonByToken(singleAccessToken)).toEqual(expect.objectContaining(person)) }) }) describe('when Single Access Token is invalid', () => { it('throws an error', async () => { mockPersonToken(singleAccessToken.token, 422) await expect(api.getPersonByToken(singleAccessToken)).rejects.toMatchObject({ message: 'Erro ao tentar buscar os dados do aluno na SmartFit.', statusCode: 422, }) }) }) }) describe('#getByLogin', () => { const cpf = '21857005880' describe('when input login is valid', () => { it('requests and returns a SmartFit`s Single Access Token for Person access', async () => { mockTokenEndpoint({ auth_token: singleAccessToken.token }) expect(await api.getByLogin(cpf)).toEqual(singleAccessToken) }) }) describe('when input login is invalid', () => { it('throws an error', async () => { mockTokenEndpoint(422) await expect(api.getByLogin(cpf)).rejects.toMatchObject({ message: `Erro ao tentar realizar o login de ${cpf} na SmartFit.`, statusCode: 422, }) }) }) }) describe('#signIn', () => { const login = '21857005880' const password = internet.password() describe('when input login and password is valid', () => { it('requests and returns a SmartFit`s Single Access Token for Person access', async () => { mockTokenEndpoint({ auth_token: singleAccessToken.token }) expect(await api.signIn({ login, password })).toEqual(singleAccessToken) }) }) describe('when input login and password is invalid', () => { it('throws an error', async () => { mockTokenEndpoint(422) await expect(api.getByLogin(login)).rejects.toMatchObject({ message: `Erro ao tentar realizar o login de ${login} na SmartFit.`, statusCode: 422, }) }) }) }) describe('#findAddons', () => { let addons: Array function mockFindAddons(accessToken: any, response: any) { myFetchMock.getOnce(endpoint(`people/${accessToken}/addons`), response, { headers: headers }) } describe('when Single Access Token is valid', () => { it('requests and returns the list of addons from the student', async () => { addons = await factory.buildMany('addon', 2) mockFindAddons( singleAccessToken.token, addons.map((addon: any) => { const requestAddon = {} as any requestAddon.id = addon.id requestAddon.addon_id = addon.addonId requestAddon.purchase_id = addon.purchaseId requestAddon.created_at = addon.createdAt requestAddon.updated_at = addon.updatedAt requestAddon.expires_at = addon.expiresAt requestAddon.cancelled_at = addon.cancelledAt requestAddon.free_days_expired_at = addon.freeDaysExpiredAt requestAddon.original_price = addon.originalPrice requestAddon.subscribe_location_id = addon.subscribeLocationId requestAddon.free_days = addon.freeDays requestAddon.origin = addon.origin return { purchase_addon: requestAddon } }), ) expect(await api.findAddons(singleAccessToken)).toEqual(addons) }) }) describe('when Single Access Token is invalid', () => { it('throws an error', async () => { mockFindAddons(singleAccessToken.token, 422) await expect(api.findAddons(singleAccessToken)).rejects.toMatchObject({ message: 'Erro ao tentar buscar addons do aluno na SmartFit.', statusCode: 422, }) }) }) }) describe('#removeAddon', () => { const addonCode = datatype.uuid() function mockRemoveAddon(accessToken: any, addonCode: string, response: any) { myFetchMock.deleteOnce(endpoint(`people/${accessToken}/addons/${addonCode}`), response, { headers: headers }) } describe('when single access token is valid', () => { describe('with an valid addon code', () => { it('requests and return a success response', async () => { const successResponse = { success: true } mockRemoveAddon(singleAccessToken.token, addonCode, successResponse) expect(await api.removeAddon(singleAccessToken, addonCode)).toEqual(successResponse) }) }) }) describe('when addon code is invalid', () => { it('throws an error', async () => { mockRemoveAddon(singleAccessToken.token, addonCode, 404) await expect(api.removeAddon(singleAccessToken, addonCode)).rejects.toMatchObject({ message: `Erro ao tentar remover addon ${addonCode} do aluno na SmartFit.`, statusCode: 404, }) }) }) describe('when removals is unsuccessful', () => { it('throws an error', async () => { mockRemoveAddon(singleAccessToken.token, addonCode, 422) await expect(api.removeAddon(singleAccessToken, addonCode)).rejects.toMatchObject({ message: `Erro ao tentar remover addon ${addonCode} do aluno na SmartFit.`, statusCode: 422, }) }) }) }) describe('#addAddon', () => { const addonCode = datatype.uuid() let addAddon: jest.SpyInstance function mockAddAddon(response: any, config?: any) { myFetchMock.post(endpoint(`people/addons`), response, { headers: headers, ...config }) } beforeEach(() => { addAddon = jest.spyOn(api, 'addAddon') }) afterEach(() => { addAddon.mockRestore() }) describe('when single access token', () => { const successResponse = { success: true } describe('with an valid addon code', () => { it('requests and add the addon on student', async () => { mockAddAddon(successResponse) expect(await api.addAddon(singleAccessToken, addonCode)).toEqual(successResponse) }) }) describe('when throws an error', () => { beforeEach(() => { mockAddAddon({ throws: { message: 'Falha ao adicionar addon' }, status: 404 }) }) describe('when DIGITAL location acronym not throws an error', () => { it('tries add addon with DIGITAL location acronym', async () => { try { await api.addAddon(singleAccessToken, addonCode) } catch (error) { } finally { expect(addAddon).toHaveBeenNthCalledWith(2, singleAccessToken, addonCode, 'DIGITAL') } }) }) describe('when DIGITAL location acronym also throws an error', () => { it('tries add addon with DIGITAL location acronym', async () => { try { await api.addAddon(singleAccessToken, addonCode) } catch (error) { } finally { expect(addAddon).not.toHaveBeenNthCalledWith(3, {}) } }) }) }) }) }) describe('#getWalletToken', () => { const token = random.alphaNumeric(10) describe('when the endpoint returns successfully', () => { const result = { token: random.alphaNumeric(), kind: 'credit', source: 'ingenico' } beforeEach(() => { myFetchMock.getOnce(endpoint(`wallet_tokens/${token}`, true), { ...result, kind: 'credit_card' }) }) it('requests with privated api', async () => { await api.getWalletToken(token) expect(myFetchMock).toBeCalledWith( endpoint(`wallet_tokens/${token}`, true), expect.objectContaining({ headers: expect.objectContaining({ Authorization: `Token token=${globalThis.secret.smartfit.privated.token.authorization}`, }), }), ) }) it('returns the token', async () => { await expect(api.getWalletToken(token)).resolves.toEqual(result) }) }) describe('when the endpoint returns an error', () => { beforeEach(() => { myFetchMock.getOnce(endpoint(`wallet_tokens/${token}`, true), 404) }) it('in a prod environment it throws an error', async () => { process.env.STAGE = 'prod' await expect(api.getWalletToken(token)).rejects.toMatchObject( expect.objectContaining({ message: 'Erro ao tentar buscar o token do cartão do aluno na SmartFit.' }), ) }) it('in not a prod environment it returns a null token ', async () => { process.env.STAGE = 'dev' await expect(api.getWalletToken(token)).resolves.toEqual({ token: 'mock', kind: 'credit' }) }) }) }) describe('#createIntegration', () => { const id = datatype.uuid() const document = random.alphaNumeric() const email = internet.email() const status: ForeignIntegrationStatus = sample(['active', 'failed', 'cancelled']) let update = false let params: [string, string, string, ForeignIntegrationStatus, boolean] beforeEach(() => { params = [id, document, email, status, update] }) describe('when is not an update', () => { beforeEach(() => { myFetchMock.postOnce(endpoint('person/foreign_integrations', true), {}) }) it('requests with privated api', async () => { await api.createIntegration(...params) expect(myFetchMock).toBeCalledWith( endpoint(`person/foreign_integrations`, true), expect.objectContaining({ headers: expect.objectContaining({ Authorization: `Token token=${globalThis.secret.smartfit.privated.token.authorization}`, }), }), ) }) it('requests with correct post data', async () => { await api.createIntegration(...params) expect(myFetchMock).toBeCalledWith( endpoint(`person/foreign_integrations`, true), expect.objectContaining({ body: JSON.stringify({ id, document, email, status }), }), ) }) describe('when endpoint returns an error', () => { beforeEach(() => { myFetchMock.postOnce(endpoint('person/foreign_integrations', true), 402) }) it('throws an error', async () => { await expect(api.createIntegration(...params)).rejects.toMatchObject( expect.objectContaining({ message: 'Erro ao realizar uma integração de status do aluno na SmartFit.' }), ) }) }) }) describe('when is an update', () => { beforeAll(() => { update = true }) beforeEach(() => { myFetchMock.patchOnce(endpoint('person/foreign_integrations', true), {}) }) it('requests with privated api', async () => { await api.createIntegration(...params) expect(myFetchMock).toBeCalledWith( endpoint(`person/foreign_integrations`, true), expect.objectContaining({ method: 'PATCH', headers: expect.objectContaining({ Authorization: `Token token=${globalThis.secret.smartfit.privated.token.authorization}`, }), }), ) }) it('requests with correct patch data', async () => { await api.createIntegration(...params) expect(myFetchMock).toBeCalledWith( endpoint(`person/foreign_integrations`, true), expect.objectContaining({ body: JSON.stringify({ id, status }), }), ) }) describe('when endpoint returns an error', () => { beforeEach(() => { myFetchMock.patchOnce(endpoint('person/foreign_integrations', true), 402) }) it('throws an error', async () => { await expect(api.createIntegration(...params)).rejects.toMatchObject( expect.objectContaining({ message: 'Erro ao realizar uma integração de status do aluno na SmartFit.' }), ) }) }) }) }) })