diff --git a/lib/idx/run.ts b/lib/idx/run.ts index d12d919e3..dd295be72 100644 --- a/lib/idx/run.ts +++ b/lib/idx/run.ts @@ -32,6 +32,7 @@ import { import { IdxMessage, IdxResponse, isIdxResponse } from './types/idx-js'; import { getSavedTransactionMeta, saveTransactionMeta } from './transactionMeta'; import { getAvailableSteps, getEnabledFeatures, getMessagesFromResponse, isTerminalResponse } from './util'; + declare interface RunData { options: RunOptions; values: remediators.RemediationValues; @@ -257,9 +258,15 @@ async function finalizeData(authClient, data: RunData): Promise { const hasActions = Object.keys(idxResponse!.actions).length > 0; const hasErrors = !!messages.find(msg => msg.class === 'ERROR'); const isTerminalSuccess = !hasActions && !hasErrors && idxResponse!.requestDidSucceed === true; + const isTerminalFailure = !hasActions && hasErrors && idxResponse!.requestDidSucceed === false; if (isTerminalSuccess) { shouldClearTransaction = true; - } else { + } + else if (isTerminalFailure) { + shouldClearTransaction = true; + status = IdxStatus.FAILURE; + } + else { // only save response if there are actions available (ignore messages) shouldSaveResponse = shouldSaveResponse && hasActions; } @@ -279,6 +286,12 @@ async function finalizeData(authClient, data: RunData): Promise { shouldClearTransaction = true; } } + + if (status === IdxStatus.FAILURE) { + clearSharedStorage = true; + shouldClearTransaction = true; + } + return { ...data, status, diff --git a/test/spec/idx/run.ts b/test/spec/idx/run.ts index 2b1f05f7e..ee3758dd5 100644 --- a/test/spec/idx/run.ts +++ b/test/spec/idx/run.ts @@ -60,7 +60,7 @@ describe('idx/run', () => { const remediateResponse = { idxResponse, - nextStep: 'remediate-nextStep' + // nextStep: 'remediate-nextStep' }; jest.spyOn(mocked.remediate, 'remediate').mockResolvedValue(remediateResponse); @@ -100,103 +100,12 @@ describe('idx/run', () => { }; }); - describe('with stateHandle', () => { - it('will call introspect', async () => { - const { authClient } = testContext; - const stateHandle = 'abc'; - await run(authClient, { stateHandle }); - expect(mocked.introspect.introspect).toHaveBeenCalledWith(authClient, { - withCredentials: true, - stateHandle - }); - }); - - it('does not call interact', async () => { - const { authClient } = testContext; - const stateHandle = 'abc'; - await run(authClient, { stateHandle }); - expect(mocked.interact.interact).not.toHaveBeenCalled(); - }); - - it('will preserve transaction meta, if it exists', async () => { - const { authClient, transactionMeta } = testContext; - jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(transactionMeta); - jest.spyOn(mocked.transactionMeta, 'saveTransactionMeta'); - const stateHandle = 'abc'; - await run(authClient, { stateHandle }); - expect(mocked.transactionMeta.saveTransactionMeta).toHaveBeenCalledWith(authClient, transactionMeta); - }); - }); - - describe('flow', () => { - it('if not specified or already set, sets the flow to "default"', async () => { - const { authClient } = testContext; - jest.spyOn(authClient.idx, 'setFlow'); - await run(authClient); - expect(authClient.idx.setFlow).toHaveBeenCalledWith('default'); - }); - - it('if flow is set in run options, it sets the flow on the authClient', async () => { - const { authClient } = testContext; - const flow = 'signup'; - jest.spyOn(authClient.idx, 'setFlow'); - await run(authClient, { flow }); - expect(authClient.idx.setFlow).toHaveBeenCalledWith(flow); - }); - - it('if flow is not set in run options, it respects the flow already set on auth client', async () => { - const { authClient } = testContext; - jest.spyOn(authClient.idx, 'getFlow').mockReturnValue('existing'); - jest.spyOn(authClient.idx, 'setFlow'); - await run(authClient); - expect(authClient.idx.setFlow).toHaveBeenCalledWith('existing'); - }); - - it('retrieves flow specification based on flow option', async () => { - const { authClient } = testContext; - jest.spyOn(mocked.FlowSpecification, 'getFlowSpecification'); - await run(authClient, { flow: 'signup' }); - expect(mocked.FlowSpecification.getFlowSpecification).toHaveBeenCalledWith(authClient, 'signup'); - }); - }); - - describe('with saved transaction', () => { - beforeEach(() => { - const { transactionMeta } = testContext; - jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(transactionMeta); - }); - it('if saved meta has no interactionHandle, will call interact', async () => { - const { authClient } = testContext; - await run(authClient); - expect(mocked.interact.interact).toHaveBeenCalled(); - }); - it('if saved meta has interactionHandle, does not call interact', async () => { - const { authClient, transactionMeta } = testContext; - transactionMeta.interactionHandle = 'fake'; - await run(authClient); - expect(mocked.interact.interact).not.toHaveBeenCalled(); - }); - }); - - describe('no saved transaction', () => { - beforeEach(() => { - jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(undefined); - }); - it('clears saved transaction data and calls interact', async () => { - const { authClient } = testContext; - jest.spyOn(authClient.transactionManager, 'clear'); - await run(authClient); - expect(authClient.transactionManager.clear).toHaveBeenCalledTimes(1); - expect(mocked.interact.interact).toHaveBeenCalled(); - }); - }); - it('returns transaction', async () => { const { authClient } = testContext; const res = await run(authClient); expect(res).toMatchObject({ status: IdxStatus.PENDING, - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), requestDidSucceed: true }); }); @@ -318,11 +227,122 @@ describe('idx/run', () => { const res = await run(authClient); expect(res).toMatchObject({ messages: ['remediate-message-1'], - nextStep: 'remediate-nextStep', status: IdxStatus.PENDING, }); }); + describe('with stateHandle', () => { + it('will call introspect', async () => { + const { authClient } = testContext; + const stateHandle = 'abc'; + await run(authClient, { stateHandle }); + expect(mocked.introspect.introspect).toHaveBeenCalledWith(authClient, { + withCredentials: true, + stateHandle + }); + }); + + it('does not call interact', async () => { + const { authClient } = testContext; + const stateHandle = 'abc'; + await run(authClient, { stateHandle }); + expect(mocked.interact.interact).not.toHaveBeenCalled(); + }); + + it('will preserve transaction meta, if it exists', async () => { + const { authClient, transactionMeta } = testContext; + jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(transactionMeta); + jest.spyOn(mocked.transactionMeta, 'saveTransactionMeta'); + const stateHandle = 'abc'; + await run(authClient, { stateHandle }); + expect(mocked.transactionMeta.saveTransactionMeta).toHaveBeenCalledWith(authClient, transactionMeta); + }); + }); + + describe('flow', () => { + it('if not specified or already set, sets the flow to "default"', async () => { + const { authClient } = testContext; + jest.spyOn(authClient.idx, 'setFlow'); + await run(authClient); + expect(authClient.idx.setFlow).toHaveBeenCalledWith('default'); + }); + + it('if flow is set in run options, it sets the flow on the authClient', async () => { + const { authClient } = testContext; + const flow = 'signup'; + jest.spyOn(authClient.idx, 'setFlow'); + await run(authClient, { flow }); + expect(authClient.idx.setFlow).toHaveBeenCalledWith(flow); + }); + + it('if flow is not set in run options, it respects the flow already set on auth client', async () => { + const { authClient } = testContext; + jest.spyOn(authClient.idx, 'getFlow').mockReturnValue('existing'); + jest.spyOn(authClient.idx, 'setFlow'); + await run(authClient); + expect(authClient.idx.setFlow).toHaveBeenCalledWith('existing'); + }); + + it('retrieves flow specification based on flow option', async () => { + const { authClient } = testContext; + jest.spyOn(mocked.FlowSpecification, 'getFlowSpecification'); + await run(authClient, { flow: 'signup' }); + expect(mocked.FlowSpecification.getFlowSpecification).toHaveBeenCalledWith(authClient, 'signup'); + }); + }); + + describe('with saved transaction', () => { + beforeEach(() => { + const { transactionMeta } = testContext; + jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(transactionMeta); + }); + it('if saved meta has no interactionHandle, will call interact', async () => { + const { authClient } = testContext; + await run(authClient); + expect(mocked.interact.interact).toHaveBeenCalled(); + }); + it('if saved meta has interactionHandle, does not call interact', async () => { + const { authClient, transactionMeta } = testContext; + transactionMeta.interactionHandle = 'fake'; + await run(authClient); + expect(mocked.interact.interact).not.toHaveBeenCalled(); + }); + + it('if saved meta has interactionHandle and introspect fails, clears transaction', async () => { + const { authClient, transactionMeta } = testContext; + transactionMeta.interactionHandle = 'fake'; + const expiredInteractionHandleResponse = IdxResponseFactory.build({ + neededToProceed: [], + requestDidSucceed: false, + rawIdxState: { + messages: { + value: [{class: 'ERROR', i18n: { key: 'idx.session.expired' }, message: 'foo'}] + } + } + }); + jest.spyOn(mocked.introspect, 'introspect').mockResolvedValue(expiredInteractionHandleResponse); + jest.spyOn(mocked.remediate, 'remediate').mockResolvedValue({idxResponse: expiredInteractionHandleResponse}); + jest.spyOn(authClient.transactionManager, 'clear'); + const res = await run(authClient); + expect(mocked.interact.interact).not.toHaveBeenCalled(); + expect(authClient.transactionManager.clear).toHaveBeenCalled(); + expect(res.status).toBe(IdxStatus.FAILURE); + }); + }); + + describe('no saved transaction', () => { + beforeEach(() => { + jest.spyOn(mocked.transactionMeta, 'getSavedTransactionMeta').mockReturnValue(undefined); + }); + it('clears saved transaction data and calls interact', async () => { + const { authClient } = testContext; + jest.spyOn(authClient.transactionManager, 'clear'); + await run(authClient); + expect(authClient.transactionManager.clear).toHaveBeenCalledTimes(1); + expect(mocked.interact.interact).toHaveBeenCalled(); + }); + }); + describe('response is not terminal', () => { beforeEach(() => { const { transactionMeta } = testContext; @@ -338,7 +358,7 @@ describe('idx/run', () => { const res = await run(authClient); expect(authClient.transactionManager.clear).not.toHaveBeenCalledWith(); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.PENDING, }); }); @@ -384,7 +404,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.PENDING, }); expect(authClient.transactionManager.clear).not.toHaveBeenCalled(); @@ -445,7 +465,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).toHaveBeenCalledTimes(1); @@ -476,7 +496,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).toHaveBeenCalledTimes(1); @@ -510,7 +530,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).not.toHaveBeenCalled(); @@ -539,7 +559,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).not.toHaveBeenCalled(); @@ -579,7 +599,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).not.toHaveBeenCalled(); @@ -611,7 +631,7 @@ describe('idx/run', () => { jest.spyOn(authClient.transactionManager, 'clear'); const res = await run(authClient); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.TERMINAL, }); expect(authClient.transactionManager.clear).not.toHaveBeenCalled(); @@ -655,7 +675,7 @@ describe('idx/run', () => { }); expect(res).toMatchObject({ - nextStep: 'remediate-nextStep', + rawIdxState: expect.any(Object), status: IdxStatus.SUCCESS, tokens: tokenResponse.tokens, }); diff --git a/test/support/idx/factories/responses.ts b/test/support/idx/factories/responses.ts index 4e414486e..9d4f74dfe 100644 --- a/test/support/idx/factories/responses.ts +++ b/test/support/idx/factories/responses.ts @@ -50,7 +50,8 @@ export const IdxResponseFactory = Factory.define