diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json index ad1c96efa..673cac606 100644 --- a/config/identity/handler/default.json +++ b/config/identity/handler/default.json @@ -18,18 +18,18 @@ { "@id": "urn:solid-server:default:IdentityProviderHttpHandler", "@type": "IdentityProviderHttpHandler", - "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "idpPath": "/idp", - "requestParser": { "@id": "urn:solid-server:default:RequestParser" }, - "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, - "converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, - "interactionCompleter": { + "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "args_idpPath": "/idp", + "args_requestParser": { "@id": "urn:solid-server:default:RequestParser" }, + "args_providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, + "args_converter": { "@id": "urn:solid-server:default:RepresentationConverter" }, + "args_interactionCompleter": { "comment": "Responsible for finishing OIDC interactions.", "@type": "InteractionCompleter", "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" } }, - "errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, - "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" } + "args_errorHandler": { "@id": "urn:solid-server:default:ErrorHandler" }, + "args_responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" } } ] } diff --git a/config/identity/handler/interaction/routes.json b/config/identity/handler/interaction/routes.json index edf4c366e..c05a0f919 100644 --- a/config/identity/handler/interaction/routes.json +++ b/config/identity/handler/interaction/routes.json @@ -9,7 +9,7 @@ "@graph": [ { "@id": "urn:solid-server:default:IdentityProviderHttpHandler", - "IdentityProviderHttpHandler:_interactionRoutes": [ + "IdentityProviderHttpHandler:_args_interactionRoutes": [ { "@id": "urn:solid-server:auth:password:ForgotPasswordRoute" }, { "@id": "urn:solid-server:auth:password:LoginRoute" }, { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }, diff --git a/config/identity/registration/enabled.json b/config/identity/registration/enabled.json index 4fd3ca03d..58155291a 100644 --- a/config/identity/registration/enabled.json +++ b/config/identity/registration/enabled.json @@ -7,7 +7,7 @@ { "comment": "Enable registration by adding a registration handler to the list of interaction routes.", "@id": "urn:solid-server:default:IdentityProviderHttpHandler", - "IdentityProviderHttpHandler:_interactionRoutes": [ + "IdentityProviderHttpHandler:_args_interactionRoutes": [ { "@id": "urn:solid-server:auth:password:RegistrationRoute" } ] } diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts index f58566360..a6bf32392 100644 --- a/src/identity/IdentityProviderHttpHandler.ts +++ b/src/identity/IdentityProviderHttpHandler.ts @@ -6,16 +6,15 @@ import type { ResponseDescription } from '../ldp/http/response/ResponseDescripti import type { ResponseWriter } from '../ldp/http/ResponseWriter'; import type { Operation } from '../ldp/operations/Operation'; import { BasicRepresentation } from '../ldp/representation/BasicRepresentation'; -import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; import { getLoggerFor } from '../logging/LogUtil'; -import type { HttpHandlerInput } from '../server/HttpHandler'; -import { HttpHandler } from '../server/HttpHandler'; +import { BaseHttpHandler } from '../server/BaseHttpHandler'; +import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler'; import type { HttpRequest } from '../server/HttpRequest'; import type { HttpResponse } from '../server/HttpResponse'; import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter'; import { APPLICATION_JSON } from '../util/ContentTypes'; import { BadRequestHttpError } from '../util/errors/BadRequestHttpError'; -import { assertError, createErrorMessage } from '../util/errors/ErrorUtil'; +import { createErrorMessage } from '../util/errors/ErrorUtil'; import { InternalServerError } from '../util/errors/InternalServerError'; import { joinUrl, trimTrailingSlashes } from '../util/PathUtil'; import { addTemplateMetadata } from '../util/ResourceUtil'; @@ -64,6 +63,37 @@ export class InteractionRoute { } } +export interface IdentityProviderHttpHandlerArgs extends BaseHttpHandlerArgs { + // Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73 + requestParser: RequestParser; + errorHandler: ErrorHandler; + responseWriter: ResponseWriter; + /** + * Base URL of the server. + */ + baseUrl: string; + /** + * Relative path of the IDP entry point. + */ + idpPath: string; + /** + * Used to generate the OIDC provider. + */ + providerFactory: ProviderFactory; + /** + * All routes handling the custom IDP behaviour. + */ + interactionRoutes: InteractionRoute[]; + /** + * Used for content negotiation. + */ + converter: RepresentationConverter; + /** + * Used for POST requests that need to be handled by the OIDC library. + */ + interactionCompleter: InteractionCompleter; +} + /** * Handles all requests relevant for the entire IDP interaction, * by sending them to either a matching {@link InteractionRoute}, @@ -76,71 +106,32 @@ export class InteractionRoute { * This handler handles all requests since it assumes all those requests are relevant for the IDP interaction. * A {@link RouterHandler} should be used to filter out other requests. */ -export class IdentityProviderHttpHandler extends HttpHandler { +export class IdentityProviderHttpHandler extends BaseHttpHandler { protected readonly logger = getLoggerFor(this); private readonly baseUrl: string; - private readonly requestParser: RequestParser; private readonly providerFactory: ProviderFactory; private readonly interactionRoutes: InteractionRoute[]; private readonly converter: RepresentationConverter; private readonly interactionCompleter: InteractionCompleter; - private readonly errorHandler: ErrorHandler; - private readonly responseWriter: ResponseWriter; - /** - * @param baseUrl - Base URL of the server. - * @param idpPath - Relative path of the IDP entry point. - * @param requestParser - Used for parsing requests. - * @param providerFactory - Used to generate the OIDC provider. - * @param interactionRoutes - All routes handling the custom IDP behaviour. - * @param converter - Used for content negotiation.. - * @param interactionCompleter - Used for POST requests that need to be handled by the OIDC library. - * @param errorHandler - Converts errors to responses. - * @param responseWriter - Renders error responses. - */ - public constructor( - baseUrl: string, - idpPath: string, - requestParser: RequestParser, - providerFactory: ProviderFactory, - interactionRoutes: InteractionRoute[], - converter: RepresentationConverter, - interactionCompleter: InteractionCompleter, - errorHandler: ErrorHandler, - responseWriter: ResponseWriter, - ) { - super(); + public constructor(args: IdentityProviderHttpHandlerArgs) { + // It is important that the RequestParser does not read out the Request body stream. + // Otherwise we can't pass it anymore to the OIDC library when needed. + super(args); // Trimming trailing slashes so the relative URL starts with a slash after slicing this off - this.baseUrl = trimTrailingSlashes(joinUrl(baseUrl, idpPath)); - this.requestParser = requestParser; - this.providerFactory = providerFactory; - this.interactionRoutes = interactionRoutes; - this.converter = converter; - this.interactionCompleter = interactionCompleter; - this.errorHandler = errorHandler; - this.responseWriter = responseWriter; - } - - public async handle({ request, response }: HttpHandlerInput): Promise { - let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }}; - try { - // It is important that this RequestParser does not read out the Request body stream. - // Otherwise we can't pass it anymore to the OIDC library when needed. - const operation = await this.requestParser.handleSafe(request); - ({ preferences } = operation); - await this.handleOperation(operation, request, response); - } catch (error: unknown) { - assertError(error); - const result = await this.errorHandler.handleSafe({ error, preferences }); - await this.responseWriter.handleSafe({ response, result }); - } + this.baseUrl = trimTrailingSlashes(joinUrl(args.baseUrl, args.idpPath)); + this.providerFactory = args.providerFactory; + this.interactionRoutes = args.interactionRoutes; + this.converter = args.converter; + this.interactionCompleter = args.interactionCompleter; } /** * Finds the matching route and resolves the operation. */ - private async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse): Promise { + protected async handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse): + Promise { // This being defined means we're in an OIDC session let oidcInteraction: Interaction | undefined; try { @@ -155,7 +146,11 @@ export class IdentityProviderHttpHandler extends HttpHandler { if (!route) { const provider = await this.providerFactory.getProvider(); this.logger.debug(`Sending request to oidc-provider: ${request.url}`); - return provider.callback(request, response); + // Even though the typings do not indicate this, this is a Promise that needs to be awaited. + // Otherwise the `BaseHttpServerFactory` will write a 404 before the OIDC library could handle the response. + // eslint-disable-next-line @typescript-eslint/await-thenable + await provider.callback(request, response); + return; } // IDP handlers expect JSON data @@ -169,9 +164,7 @@ export class IdentityProviderHttpHandler extends HttpHandler { } const { result, templateFiles } = await this.resolveRoute(operation, route, oidcInteraction); - const responseDescription = - await this.handleInteractionResult(operation, request, result, templateFiles, oidcInteraction); - await this.responseWriter.handleSafe({ response, result: responseDescription }); + return this.handleInteractionResult(operation, request, result, templateFiles, oidcInteraction); } /** diff --git a/src/index.ts b/src/index.ts index 900599983..20d1926c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,6 +197,7 @@ export * from './pods/GeneratedPodManager'; export * from './pods/PodManager'; // Server +export * from './server/BaseHttpHandler'; export * from './server/BaseHttpServerFactory'; export * from './server/HttpHandler'; export * from './server/HttpRequest'; diff --git a/src/ldp/AuthenticatedLdpHandler.ts b/src/ldp/AuthenticatedLdpHandler.ts index 976854776..0c594b9e2 100644 --- a/src/ldp/AuthenticatedLdpHandler.ts +++ b/src/ldp/AuthenticatedLdpHandler.ts @@ -1,12 +1,10 @@ import type { Credentials } from '../authentication/Credentials'; import type { CredentialsExtractor } from '../authentication/CredentialsExtractor'; import type { Authorizer } from '../authorization/Authorizer'; -import { getLoggerFor } from '../logging/LogUtil'; +import { BaseHttpHandler } from '../server/BaseHttpHandler'; +import type { BaseHttpHandlerArgs } from '../server/BaseHttpHandler'; import type { HttpHandlerInput } from '../server/HttpHandler'; -import { HttpHandler } from '../server/HttpHandler'; import type { HttpRequest } from '../server/HttpRequest'; -import type { HttpResponse } from '../server/HttpResponse'; -import { assertError } from '../util/errors/ErrorUtil'; import type { ErrorHandler } from './http/ErrorHandler'; import type { RequestParser } from './http/RequestParser'; import type { ResponseDescription } from './http/response/ResponseDescription'; @@ -15,16 +13,12 @@ import type { Operation } from './operations/Operation'; import type { OperationHandler } from './operations/OperationHandler'; import type { PermissionSet } from './permissions/PermissionSet'; import type { PermissionsExtractor } from './permissions/PermissionsExtractor'; -import type { RepresentationPreferences } from './representation/RepresentationPreferences'; -/** - * Collection of handlers needed for {@link AuthenticatedLdpHandler} to function. - */ -export interface AuthenticatedLdpHandlerArgs { - /** - * Parses the incoming requests. - */ +export interface AuthenticatedLdpHandlerArgs extends BaseHttpHandlerArgs { + // Workaround for https://github.com/LinkedSoftwareDependencies/Components-Generator.js/issues/73 requestParser: RequestParser; + errorHandler: ErrorHandler; + responseWriter: ResponseWriter; /** * Extracts the credentials from the incoming request. */ @@ -41,36 +35,27 @@ export interface AuthenticatedLdpHandlerArgs { * Executed the operation. */ operationHandler: OperationHandler; - /** - * Converts errors to a serializable format. - */ - errorHandler: ErrorHandler; - /** - * Writes out the response of the operation. - */ - responseWriter: ResponseWriter; } /** * The central manager that connects all the necessary handlers to go from an incoming request to an executed operation. */ -export class AuthenticatedLdpHandler extends HttpHandler { - private readonly requestParser!: RequestParser; - private readonly credentialsExtractor!: CredentialsExtractor; - private readonly permissionsExtractor!: PermissionsExtractor; - private readonly authorizer!: Authorizer; - private readonly operationHandler!: OperationHandler; - private readonly errorHandler!: ErrorHandler; - private readonly responseWriter!: ResponseWriter; - private readonly logger = getLoggerFor(this); +export class AuthenticatedLdpHandler extends BaseHttpHandler { + private readonly credentialsExtractor: CredentialsExtractor; + private readonly permissionsExtractor: PermissionsExtractor; + private readonly authorizer: Authorizer; + private readonly operationHandler: OperationHandler; /** * Creates the handler. * @param args - The handlers required. None of them are optional. */ public constructor(args: AuthenticatedLdpHandlerArgs) { - super(); - Object.assign(this, args); + super(args); + this.credentialsExtractor = args.credentialsExtractor; + this.permissionsExtractor = args.permissionsExtractor; + this.authorizer = args.authorizer; + this.operationHandler = args.operationHandler; } /** @@ -85,61 +70,14 @@ export class AuthenticatedLdpHandler extends HttpHandler { } /** - * Handles the incoming request and writes out the response. + * Handles the incoming operation and generates a response. * This includes the following steps: - * - Parsing the request to an Operation. * - Extracting credentials from the request. * - Extracting the required permissions. * - Validating if this operation is allowed. * - Executing the operation. - * - Writing out the response. - * @param input - The incoming request and response object to write to. - * - * @returns A promise resolving when the handling is finished. */ - public async handle(input: HttpHandlerInput): Promise { - let writeData: { response: HttpResponse; result: ResponseDescription }; - - try { - writeData = { response: input.response, result: await this.runHandlers(input.request) }; - } catch (error: unknown) { - assertError(error); - // We don't know the preferences yet at this point - const preferences: RepresentationPreferences = { type: { 'text/plain': 1 }}; - const result = await this.errorHandler.handleSafe({ error, preferences }); - writeData = { response: input.response, result }; - } - - await this.responseWriter.handleSafe(writeData); - } - - /** - * Runs all handlers except writing the output to the response. - * This because any errors thrown here have an impact on the response. - * @param request - Incoming request. - * - * @returns A promise resolving to the generated Operation. - */ - private async runHandlers(request: HttpRequest): Promise { - this.logger.verbose(`Handling LDP request for ${request.url}`); - - const operation: Operation = await this.requestParser.handleSafe(request); - this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`); - - try { - return await this.handleOperation(request, operation); - } catch (error: unknown) { - assertError(error); - return await this.errorHandler.handleSafe({ error, preferences: operation.preferences }); - } - } - - /** - * Handles the operation object. - * Runs all non-RequestParser handlers. - * This way the preferences can be used in case an error needs to be written. - */ - private async handleOperation(request: HttpRequest, operation: Operation): Promise { + protected async handleOperation(operation: Operation, request: HttpRequest): Promise { const credentials: Credentials = await this.credentialsExtractor.handleSafe(request); this.logger.verbose(`Extracted credentials: ${credentials.webId}`); diff --git a/src/server/BaseHttpHandler.ts b/src/server/BaseHttpHandler.ts new file mode 100644 index 000000000..13255c725 --- /dev/null +++ b/src/server/BaseHttpHandler.ts @@ -0,0 +1,72 @@ +import type { ErrorHandler } from '../ldp/http/ErrorHandler'; +import type { RequestParser } from '../ldp/http/RequestParser'; +import type { ResponseDescription } from '../ldp/http/response/ResponseDescription'; +import type { ResponseWriter } from '../ldp/http/ResponseWriter'; +import type { Operation } from '../ldp/operations/Operation'; +import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; +import { getLoggerFor } from '../logging/LogUtil'; +import { assertError } from '../util/errors/ErrorUtil'; +import type { HttpHandlerInput } from './HttpHandler'; +import { HttpHandler } from './HttpHandler'; +import type { HttpRequest } from './HttpRequest'; +import type { HttpResponse } from './HttpResponse'; + +export interface BaseHttpHandlerArgs { + /** + * Parses the incoming requests. + */ + requestParser: RequestParser; + /** + * Converts errors to a serializable format. + */ + errorHandler: ErrorHandler; + /** + * Writes out the response of the operation. + */ + responseWriter: ResponseWriter; +} + +/** + * Parses requests and sends the resulting Operation to the abstract `handleOperation` function. + * Errors are caught and handled by the Errorhandler. + * In case the `handleOperation` function returns a result it will be sent to the ResponseWriter. + */ +export abstract class BaseHttpHandler extends HttpHandler { + protected readonly logger = getLoggerFor(this); + + protected readonly requestParser: RequestParser; + protected readonly errorHandler: ErrorHandler; + protected readonly responseWriter: ResponseWriter; + + protected constructor(args: BaseHttpHandlerArgs) { + super(); + this.requestParser = args.requestParser; + this.errorHandler = args.errorHandler; + this.responseWriter = args.responseWriter; + } + + public async handle({ request, response }: HttpHandlerInput): Promise { + let result: ResponseDescription | undefined; + let preferences: RepresentationPreferences = { type: { 'text/plain': 1 }}; + + try { + const operation = await this.requestParser.handleSafe(request); + ({ preferences } = operation); + result = await this.handleOperation(operation, request, response); + this.logger.verbose(`Parsed ${operation.method} operation on ${operation.target.path}`); + } catch (error: unknown) { + assertError(error); + result = await this.errorHandler.handleSafe({ error, preferences }); + } + + if (result) { + await this.responseWriter.handleSafe({ response, result }); + } + } + + /** + * Handles the operation. Should return a ResponseDescription if it does not handle the response itself. + */ + protected abstract handleOperation(operation: Operation, request: HttpRequest, response: HttpResponse): + Promise; +} diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts index 761021167..ac7c2e101 100644 --- a/test/unit/identity/IdentityProviderHttpHandler.test.ts +++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts @@ -1,6 +1,12 @@ import type { Provider } from 'oidc-provider'; import type { ProviderFactory } from '../../../src/identity/configuration/ProviderFactory'; -import { InteractionRoute, IdentityProviderHttpHandler } from '../../../src/identity/IdentityProviderHttpHandler'; +import type { + IdentityProviderHttpHandlerArgs, +} from '../../../src/identity/IdentityProviderHttpHandler'; +import { + InteractionRoute, + IdentityProviderHttpHandler, +} from '../../../src/identity/IdentityProviderHttpHandler'; import type { InteractionHandler } from '../../../src/identity/interaction/email-password/handler/InteractionHandler'; import { IdpInteractionError } from '../../../src/identity/interaction/util/IdpInteractionError'; import type { InteractionCompleter } from '../../../src/identity/interaction/util/InteractionCompleter'; @@ -95,17 +101,18 @@ describe('An IdentityProviderHttpHandler', (): void => { responseWriter = { handleSafe: jest.fn() } as any; - handler = new IdentityProviderHttpHandler( + const args: IdentityProviderHttpHandlerArgs = { baseUrl, idpPath, requestParser, providerFactory, - Object.values(routes), + interactionRoutes: Object.values(routes), converter, interactionCompleter, errorHandler, responseWriter, - ); + }; + handler = new IdentityProviderHttpHandler(args); }); it('calls the provider if there is no matching route.', async(): Promise => { @@ -284,17 +291,18 @@ describe('An IdentityProviderHttpHandler', (): void => { }); it('errors if no route is configured for the default prompt.', async(): Promise => { - handler = new IdentityProviderHttpHandler( + const args: IdentityProviderHttpHandlerArgs = { baseUrl, idpPath, requestParser, providerFactory, - [], + interactionRoutes: [], converter, interactionCompleter, errorHandler, responseWriter, - ); + }; + handler = new IdentityProviderHttpHandler(args); request.url = '/idp'; provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any); const error = new InternalServerError('No handler for the default session prompt has been configured.'); diff --git a/test/unit/server/BaseHttpHandler.test.ts b/test/unit/server/BaseHttpHandler.test.ts new file mode 100644 index 000000000..8e5e07a2a --- /dev/null +++ b/test/unit/server/BaseHttpHandler.test.ts @@ -0,0 +1,69 @@ +import type { ErrorHandler } from '../../../src/ldp/http/ErrorHandler'; +import type { RequestParser } from '../../../src/ldp/http/RequestParser'; +import { ResponseDescription } from '../../../src/ldp/http/response/ResponseDescription'; +import type { ResponseWriter } from '../../../src/ldp/http/ResponseWriter'; +import type { Operation } from '../../../src/ldp/operations/Operation'; +import type { BaseHttpHandlerArgs } from '../../../src/server/BaseHttpHandler'; +import { BaseHttpHandler } from '../../../src/server/BaseHttpHandler'; +import type { HttpRequest } from '../../../src/server/HttpRequest'; +import type { HttpResponse } from '../../../src/server/HttpResponse'; + +class DummyHttpHandler extends BaseHttpHandler { + public constructor(args: BaseHttpHandlerArgs) { + super(args); + } + + public async handleOperation(): Promise { + return undefined; + } +} + +describe('A BaseHttpHandler', (): void => { + const request: HttpRequest = {} as any; + const response: HttpResponse = {} as any; + const preferences = { type: { 'text/html': 1 }}; + const operation: Operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences }; + const errorResponse = new ResponseDescription(400); + let requestParser: jest.Mocked; + let errorHandler: jest.Mocked; + let responseWriter: jest.Mocked; + let handler: jest.Mocked; + + beforeEach(async(): Promise => { + requestParser = { handleSafe: jest.fn().mockResolvedValue(operation) } as any; + errorHandler = { handleSafe: jest.fn().mockResolvedValue(errorResponse) } as any; + responseWriter = { handleSafe: jest.fn() } as any; + + handler = new DummyHttpHandler({ requestParser, errorHandler, responseWriter }) as any; + handler.handleOperation = jest.fn(); + }); + + it('calls the handleOperation function with the generated operation.', async(): Promise => { + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(handler.handleOperation).toHaveBeenCalledTimes(1); + expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response); + expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(0); + }); + + it('calls the responseWriter if there is a response.', async(): Promise => { + const result = new ResponseDescription(200); + handler.handleOperation.mockResolvedValueOnce(result); + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(handler.handleOperation).toHaveBeenCalledTimes(1); + expect(handler.handleOperation).toHaveBeenLastCalledWith(operation, request, response); + expect(errorHandler.handleSafe).toHaveBeenCalledTimes(0); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result }); + }); + + it('calls the error handler if something goes wrong.', async(): Promise => { + const error = new Error('bad data'); + handler.handleOperation.mockRejectedValueOnce(error); + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); + expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences }); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse }); + }); +});