From d3de5f3114e125d7da907fd40772d308e9d5cf46 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 3 Aug 2021 15:57:11 +0200 Subject: [PATCH] feat: Pass optional Interaction to InteractionHandlers --- .../handler/interaction/routes/session.json | 5 +-- src/identity/IdentityProviderHttpHandler.ts | 38 ++++++++++------- .../interaction/SessionHttpHandler.ts | 21 +++------- .../handler/ForgotPasswordHandler.ts | 7 ++-- .../handler/InteractionHandler.ts | 20 ++++++++- .../email-password/handler/LoginHandler.ts | 7 ++-- .../handler/RegistrationHandler.ts | 5 +-- .../handler/ResetPasswordHandler.ts | 9 ++-- .../IdentityProviderHttpHandler.test.ts | 14 ++++--- .../interaction/SessionHttpHandler.test.ts | 34 +++++---------- .../handler/ForgotPasswordHandler.test.ts | 10 ++--- .../handler/LoginHandler.test.ts | 6 ++- .../handler/RegistrationHandler.test.ts | 42 +++++++++---------- .../handler/ResetPasswordHandler.test.ts | 14 +++---- 14 files changed, 113 insertions(+), 119 deletions(-) diff --git a/config/identity/handler/interaction/routes/session.json b/config/identity/handler/interaction/routes/session.json index 8e16f17f0..8b2fe5f8a 100644 --- a/config/identity/handler/interaction/routes/session.json +++ b/config/identity/handler/interaction/routes/session.json @@ -8,10 +8,7 @@ "route": "^/confirm/?$", "prompt": "consent", "viewTemplate": "@css:templates/identity/email-password/confirm.html.ejs", - "handler": { - "@type": "SessionHttpHandler", - "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" } - } + "handler": { "@type": "SessionHttpHandler" } } ] } diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts index afe34774c..deaeb6761 100644 --- a/src/identity/IdentityProviderHttpHandler.ts +++ b/src/identity/IdentityProviderHttpHandler.ts @@ -11,9 +11,11 @@ import { assertError, createErrorMessage } from '../util/errors/ErrorUtil'; import { InternalServerError } from '../util/errors/InternalServerError'; import { trimTrailingSlashes } from '../util/PathUtil'; import type { ProviderFactory } from './configuration/ProviderFactory'; -import type { InteractionHandler, - InteractionHandlerResult } from './interaction/email-password/handler/InteractionHandler'; - +import type { + Interaction, + InteractionHandler, + InteractionHandlerResult, +} from './interaction/email-password/handler/InteractionHandler'; import { IdpInteractionError } from './interaction/util/IdpInteractionError'; import type { InteractionCompleter } from './interaction/util/InteractionCompleter'; @@ -118,33 +120,40 @@ export class IdentityProviderHttpHandler extends HttpHandler { * Finds the matching route and resolves the request. */ private async handleRequest(request: HttpRequest, response: HttpResponse): Promise { + // This being defined means we're in an OIDC session + let oidcInteraction: Interaction | undefined; + try { + const provider = await this.providerFactory.getProvider(); + oidcInteraction = await provider.interactionDetails(request, response); + } catch { + // Just a regular request + } + // If our own interaction handler does not support the input, it is either invalid or a request for the OIDC library - const route = await this.findRoute(request, response); + const route = await this.findRoute(request, oidcInteraction); if (!route) { const provider = await this.providerFactory.getProvider(); this.logger.debug(`Sending request to oidc-provider: ${request.url}`); return provider.callback(request, response); } - await this.resolveRoute(request, response, route); + await this.resolveRoute(request, response, route, oidcInteraction); } /** * Finds a route that supports the given request. */ - private async findRoute(request: HttpRequest, response: HttpResponse): Promise { + private async findRoute(request: HttpRequest, oidcInteraction?: Interaction): Promise { if (!request.url || !request.url.startsWith(this.idpPath)) { // This is either an invalid request or a call to the .well-known configuration return; } - const url = request.url.slice(this.idpPath.length); - let route = this.getRouteMatch(url); + const pathName = request.url.slice(this.idpPath.length); + let route = this.getRouteMatch(pathName); // In case the request targets the IDP entry point the prompt determines where to go - if (!route && (url === '/' || url === '')) { - const provider = await this.providerFactory.getProvider(); - const interactionDetails = await provider.interactionDetails(request, response); - route = this.getPromptMatch(interactionDetails.prompt.name); + if (!route && oidcInteraction && trimTrailingSlashes(pathName).length === 0) { + route = this.getPromptMatch(oidcInteraction.prompt.name); } return route; } @@ -155,7 +164,8 @@ export class IdentityProviderHttpHandler extends HttpHandler { * * GET requests go to the templateHandler, POST requests to the specific InteractionHandler of the route. */ - private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute): Promise { + private async resolveRoute(request: HttpRequest, response: HttpResponse, route: InteractionRoute, + oidcInteraction?: Interaction): Promise { if (request.method === 'GET') { // .ejs templates errors on undefined variables return await this.handleTemplateResponse(response, route.viewTemplate, { errorMessage: '', prefilled: {}}); @@ -164,7 +174,7 @@ export class IdentityProviderHttpHandler extends HttpHandler { if (request.method === 'POST') { let result: InteractionHandlerResult; try { - result = await route.handler.handleSafe({ request, response }); + result = await route.handler.handleSafe({ request, oidcInteraction }); } catch (error: unknown) { // Render error in the view const prefilled = IdpInteractionError.isInstance(error) ? error.prefilled : {}; diff --git a/src/identity/interaction/SessionHttpHandler.ts b/src/identity/interaction/SessionHttpHandler.ts index f74ec00c8..7980ea2ee 100644 --- a/src/identity/interaction/SessionHttpHandler.ts +++ b/src/identity/interaction/SessionHttpHandler.ts @@ -1,29 +1,18 @@ -import type { HttpHandlerInput } from '../../server/HttpHandler'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; -import type { ProviderFactory } from '../configuration/ProviderFactory'; import { InteractionHandler } from './email-password/handler/InteractionHandler'; -import type { InteractionCompleteResult } from './email-password/handler/InteractionHandler'; +import type { InteractionCompleteResult, InteractionHandlerInput } from './email-password/handler/InteractionHandler'; /** * Simple InteractionHttpHandler that sends the session accountId to the InteractionCompleter as webId. */ export class SessionHttpHandler extends InteractionHandler { - private readonly providerFactory: ProviderFactory; - - public constructor(providerFactory: ProviderFactory) { - super(); - this.providerFactory = providerFactory; - } - - public async handle(input: HttpHandlerInput): Promise { - const provider = await this.providerFactory.getProvider(); - const details = await provider.interactionDetails(input.request, input.response); - if (!details.session || !details.session.accountId) { - throw new NotImplementedHttpError('Only confirm actions with a session and accountId are supported'); + public async handle({ oidcInteraction }: InteractionHandlerInput): Promise { + if (!oidcInteraction?.session) { + throw new NotImplementedHttpError('Only interactions with a valid session are supported.'); } return { type: 'complete', - details: { webId: details.session.accountId }, + details: { webId: oidcInteraction.session.accountId }, }; } } diff --git a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts index c02e87886..211e8800f 100644 --- a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts +++ b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts @@ -1,7 +1,6 @@ import assert from 'assert'; import urljoin from 'url-join'; import { getLoggerFor } from '../../../../logging/LogUtil'; -import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import { ensureTrailingSlash } from '../../../../util/PathUtil'; import type { TemplateEngine } from '../../../../util/templates/TemplateEngine'; import type { EmailSender } from '../../util/EmailSender'; @@ -9,7 +8,7 @@ import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { throwIdpInteractionError } from '../EmailPasswordUtil'; import type { AccountStore } from '../storage/AccountStore'; import { InteractionHandler } from './InteractionHandler'; -import type { InteractionResponseResult } from './InteractionHandler'; +import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler'; export interface ForgotPasswordHandlerArgs { accountStore: AccountStore; @@ -40,10 +39,10 @@ export class ForgotPasswordHandler extends InteractionHandler { this.emailSender = args.emailSender; } - public async handle(input: HttpHandlerInput): Promise> { + public async handle({ request }: InteractionHandlerInput): Promise> { try { // Validate incoming data - const { email } = await getFormDataRequestBody(input.request); + const { email } = await getFormDataRequestBody(request); assert(typeof email === 'string' && email.length > 0, 'Email required'); await this.resetPassword(email); diff --git a/src/identity/interaction/email-password/handler/InteractionHandler.ts b/src/identity/interaction/email-password/handler/InteractionHandler.ts index bc747b0cf..58a7cb2c8 100644 --- a/src/identity/interaction/email-password/handler/InteractionHandler.ts +++ b/src/identity/interaction/email-password/handler/InteractionHandler.ts @@ -1,7 +1,23 @@ -import type { HttpHandlerInput } from '../../../../server/HttpHandler'; +import type { KoaContextWithOIDC } from 'oidc-provider'; +import type { HttpRequest } from '../../../../server/HttpRequest'; import { AsyncHandler } from '../../../../util/handlers/AsyncHandler'; import type { InteractionCompleterParams } from '../../util/InteractionCompleter'; +// OIDC library does not directly export the Interaction type +export type Interaction = KoaContextWithOIDC['oidc']['entities']['Interaction']; + +export interface InteractionHandlerInput { + /** + * The request being made. + */ + request: HttpRequest; + /** + * Will be defined if the OIDC library expects us to resolve an interaction it can't handle itself, + * such as logging a user in. + */ + oidcInteraction?: Interaction; +} + export type InteractionHandlerResult = InteractionResponseResult | InteractionCompleteResult; export interface InteractionResponseResult> { @@ -17,4 +33,4 @@ export interface InteractionCompleteResult { /** * Handler used for IDP interactions. */ -export abstract class InteractionHandler extends AsyncHandler {} +export abstract class InteractionHandler extends AsyncHandler {} diff --git a/src/identity/interaction/email-password/handler/LoginHandler.ts b/src/identity/interaction/email-password/handler/LoginHandler.ts index 579542c07..a5289cb09 100644 --- a/src/identity/interaction/email-password/handler/LoginHandler.ts +++ b/src/identity/interaction/email-password/handler/LoginHandler.ts @@ -1,12 +1,11 @@ import assert from 'assert'; import { getLoggerFor } from '../../../../logging/LogUtil'; -import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import type { HttpRequest } from '../../../../server/HttpRequest'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { throwIdpInteractionError } from '../EmailPasswordUtil'; import type { AccountStore } from '../storage/AccountStore'; import { InteractionHandler } from './InteractionHandler'; -import type { InteractionCompleteResult } from './InteractionHandler'; +import type { InteractionCompleteResult, InteractionHandlerInput } from './InteractionHandler'; /** * Handles the submission of the Login Form and logs the user in. @@ -21,8 +20,8 @@ export class LoginHandler extends InteractionHandler { this.accountStore = accountStore; } - public async handle(input: HttpHandlerInput): Promise { - const { email, password, remember } = await this.parseInput(input.request); + public async handle({ request }: InteractionHandlerInput): Promise { + const { email, password, remember } = await this.parseInput(request); try { // Try to log in, will error if email/password combination is invalid const webId = await this.accountStore.authenticate(email, password); diff --git a/src/identity/interaction/email-password/handler/RegistrationHandler.ts b/src/identity/interaction/email-password/handler/RegistrationHandler.ts index ded5dda54..16b78de21 100644 --- a/src/identity/interaction/email-password/handler/RegistrationHandler.ts +++ b/src/identity/interaction/email-password/handler/RegistrationHandler.ts @@ -5,13 +5,12 @@ import { getLoggerFor } from '../../../../logging/LogUtil'; import type { IdentifierGenerator } from '../../../../pods/generate/IdentifierGenerator'; import type { PodManager } from '../../../../pods/PodManager'; import type { PodSettings } from '../../../../pods/settings/PodSettings'; -import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import type { HttpRequest } from '../../../../server/HttpRequest'; import type { OwnershipValidator } from '../../../ownership/OwnershipValidator'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil'; import type { AccountStore } from '../storage/AccountStore'; -import type { InteractionResponseResult } from './InteractionHandler'; +import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler'; import { InteractionHandler } from './InteractionHandler'; const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/u; @@ -103,7 +102,7 @@ export class RegistrationHandler extends InteractionHandler { this.podManager = args.podManager; } - public async handle({ request }: HttpHandlerInput): Promise> { + public async handle({ request }: InteractionHandlerInput): Promise> { const result = await this.parseInput(request); try { diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts index e47a88c40..647149d88 100644 --- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts +++ b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts @@ -1,10 +1,9 @@ import assert from 'assert'; import { getLoggerFor } from '../../../../logging/LogUtil'; -import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil'; import type { AccountStore } from '../storage/AccountStore'; -import type { InteractionResponseResult } from './InteractionHandler'; +import type { InteractionResponseResult, InteractionHandlerInput } from './InteractionHandler'; import { InteractionHandler } from './InteractionHandler'; /** @@ -21,12 +20,12 @@ export class ResetPasswordHandler extends InteractionHandler { this.accountStore = accountStore; } - public async handle(input: HttpHandlerInput): Promise { + public async handle({ request }: InteractionHandlerInput): Promise { try { // Extract record ID from request URL - const recordId = /\/([^/]+)$/u.exec(input.request.url!)?.[1]; + const recordId = /\/([^/]+)$/u.exec(request.url!)?.[1]; // Validate input data - const { password, confirmPassword } = await getFormDataRequestBody(input.request); + const { password, confirmPassword } = await getFormDataRequestBody(request); assert( typeof recordId === 'string' && recordId.length > 0, 'Invalid request. Open the link from your email again', diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts index 8722ad077..eec635f49 100644 --- a/test/unit/identity/IdentityProviderHttpHandler.test.ts +++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts @@ -93,7 +93,7 @@ describe('An IdentityProviderHttpHandler', (): void => { request.method = 'POST'; await expect(handler.handle({ request, response })).resolves.toBeUndefined(); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); - expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response }); + expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request }); expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1); expect(templateHandler.handleSafe).toHaveBeenLastCalledWith( { response, templateFile: routes.response.responseTemplate, contents: { key: 'val' }}, @@ -106,7 +106,7 @@ describe('An IdentityProviderHttpHandler', (): void => { (routes.response.handler as jest.Mocked).handleSafe.mockResolvedValueOnce({ type: 'response' }); await expect(handler.handle({ request, response })).resolves.toBeUndefined(); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); - expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response }); + expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request }); expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1); expect(templateHandler.handleSafe).toHaveBeenLastCalledWith( { response, templateFile: routes.response.responseTemplate, contents: {}}, @@ -118,7 +118,7 @@ describe('An IdentityProviderHttpHandler', (): void => { request.method = 'POST'; await expect(handler.handle({ request, response })).resolves.toBeUndefined(); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1); - expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, response }); + expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request }); expect(interactionCompleter.handleSafe).toHaveBeenCalledTimes(1); expect(interactionCompleter.handleSafe).toHaveBeenLastCalledWith({ request, response, webId: 'webId' }); }); @@ -126,18 +126,22 @@ describe('An IdentityProviderHttpHandler', (): void => { it('matches paths based on prompt for requests to the root IDP.', async(): Promise => { request.url = '/idp'; request.method = 'POST'; - provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'other' }} as any); + const oidcInteraction = { prompt: { name: 'other' }}; + provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any); await expect(handler.handle({ request, response })).resolves.toBeUndefined(); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(0); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(1); + expect(routes.complete.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); }); it('uses the default route for requests to the root IDP without (matching) prompt.', async(): Promise => { request.url = '/idp'; request.method = 'POST'; - provider.interactionDetails.mockResolvedValueOnce({ prompt: { name: 'notSupported' }} as any); + const oidcInteraction = { prompt: { name: 'notSupported' }}; + provider.interactionDetails.mockResolvedValueOnce(oidcInteraction as any); await expect(handler.handle({ request, response })).resolves.toBeUndefined(); expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); + expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, oidcInteraction }); expect(routes.complete.handler.handleSafe).toHaveBeenCalledTimes(0); }); diff --git a/test/unit/identity/interaction/SessionHttpHandler.test.ts b/test/unit/identity/interaction/SessionHttpHandler.test.ts index fe3311a68..41b300205 100644 --- a/test/unit/identity/interaction/SessionHttpHandler.test.ts +++ b/test/unit/identity/interaction/SessionHttpHandler.test.ts @@ -1,45 +1,31 @@ -import type { Provider } from 'oidc-provider'; -import type { ProviderFactory } from '../../../../src/identity/configuration/ProviderFactory'; +import type { Interaction } from '../../../../src/identity/interaction/email-password/handler/InteractionHandler'; import { SessionHttpHandler } from '../../../../src/identity/interaction/SessionHttpHandler'; import type { HttpRequest } from '../../../../src/server/HttpRequest'; -import type { HttpResponse } from '../../../../src/server/HttpResponse'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; describe('A SessionHttpHandler', (): void => { const request: HttpRequest = {} as any; - const response: HttpResponse = {} as any; const webId = 'http://test.com/id#me'; - let details: any = {}; - let provider: Provider; + let oidcInteraction: Interaction; let handler: SessionHttpHandler; beforeEach(async(): Promise => { - details = { session: { accountId: webId }}; - provider = { - interactionDetails: jest.fn().mockResolvedValue(details), - } as any; + oidcInteraction = { session: { accountId: webId }} as any; - const factory: ProviderFactory = { - getProvider: jest.fn().mockResolvedValue(provider), - }; - - handler = new SessionHttpHandler(factory); + handler = new SessionHttpHandler(); }); - it('requires a session and accountId.', async(): Promise => { - details.session = undefined; - await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError); + it('requires a defined oidcInteraction with a session.', async(): Promise => { + oidcInteraction!.session = undefined; + await expect(handler.handle({ request, oidcInteraction })).rejects.toThrow(NotImplementedHttpError); - details.session = { accountId: undefined }; - await expect(handler.handle({ request, response })).rejects.toThrow(NotImplementedHttpError); + await expect(handler.handle({ request })).rejects.toThrow(NotImplementedHttpError); }); - it('calls the oidc completer with the webId in the session.', async(): Promise => { - await expect(handler.handle({ request, response })).resolves.toEqual({ + it('returns an InteractionCompleteResult when done.', async(): Promise => { + await expect(handler.handle({ request, oidcInteraction })).resolves.toEqual({ details: { webId }, type: 'complete', }); - expect(provider.interactionDetails).toHaveBeenCalledTimes(1); - expect(provider.interactionDetails).toHaveBeenLastCalledWith(request, response); }); }); diff --git a/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts index 4cac62f59..e9df67902 100644 --- a/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/ForgotPasswordHandler.test.ts @@ -4,13 +4,11 @@ import { import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender'; import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; -import type { HttpResponse } from '../../../../../../src/server/HttpResponse'; import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine'; import { createPostFormRequest } from './Util'; describe('A ForgotPasswordHandler', (): void => { let request: HttpRequest; - const response: HttpResponse = {} as any; const email = 'test@test.email'; const recordId = '123456'; const html = `Reset Password`; @@ -47,20 +45,20 @@ describe('A ForgotPasswordHandler', (): void => { it('errors on non-string emails.', async(): Promise => { request = createPostFormRequest({}); - await expect(handler.handle({ request, response })).rejects.toThrow('Email required'); + await expect(handler.handle({ request })).rejects.toThrow('Email required'); request = createPostFormRequest({ email: [ 'email', 'email2' ]}); - await expect(handler.handle({ request, response })).rejects.toThrow('Email required'); + await expect(handler.handle({ request })).rejects.toThrow('Email required'); }); it('does not send a mail if a ForgotPassword record could not be generated.', async(): Promise => { (accountStore.generateForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('error'); - await expect(handler.handle({ request, response })).resolves + await expect(handler.handle({ request })).resolves .toEqual({ type: 'response', details: { email }}); expect(emailSender.handleSafe).toHaveBeenCalledTimes(0); }); it('sends a mail if a ForgotPassword record could be generated.', async(): Promise => { - await expect(handler.handle({ request, response })).resolves + await expect(handler.handle({ request })).resolves .toEqual({ type: 'response', details: { email }}); expect(emailSender.handleSafe).toHaveBeenCalledTimes(1); expect(emailSender.handleSafe).toHaveBeenLastCalledWith({ diff --git a/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts b/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts index f4ab906ed..91bbb6ab4 100644 --- a/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/LoginHandler.test.ts @@ -1,12 +1,14 @@ +import type { + InteractionHandlerInput, +} from '../../../../../../src/identity/interaction/email-password/handler/InteractionHandler'; import { LoginHandler } from '../../../../../../src/identity/interaction/email-password/handler/LoginHandler'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; -import type { HttpHandlerInput } from '../../../../../../src/server/HttpHandler'; import { createPostFormRequest } from './Util'; describe('A LoginHandler', (): void => { const webId = 'http://alice.test.com/card#me'; const email = 'alice@test.email'; - let input: HttpHandlerInput; + let input: InteractionHandlerInput; let storageAdapter: AccountStore; let handler: LoginHandler; diff --git a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts index 8524aae3d..71a6511d7 100644 --- a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts @@ -10,7 +10,6 @@ import type { IdentifierGenerator } from '../../../../../../src/pods/generate/Id import type { PodManager } from '../../../../../../src/pods/PodManager'; import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings'; import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; -import type { HttpResponse } from '../../../../../../src/server/HttpResponse'; import { createPostFormRequest } from './Util'; describe('A RegistrationHandler', (): void => { @@ -27,7 +26,6 @@ describe('A RegistrationHandler', (): void => { const createPod = 'true'; let request: HttpRequest; - const response: HttpResponse = {} as any; const baseUrl = 'http://test.com/'; const webIdSuffix = '/profile/card'; @@ -72,71 +70,71 @@ describe('A RegistrationHandler', (): void => { describe('validating data', (): void => { it('rejects array inputs.', async(): Promise => { request = createPostFormRequest({ mydata: [ 'a', 'b' ]}); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Unexpected multiple values for mydata.'); }); it('errors on invalid emails.', async(): Promise => { request = createPostFormRequest({ email: undefined }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a valid e-mail address.'); request = createPostFormRequest({ email: '' }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a valid e-mail address.'); request = createPostFormRequest({ email: 'invalidEmail' }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a valid e-mail address.'); }); it('errors when a required WebID is not valid.', async(): Promise => { request = createPostFormRequest({ email, register, webId: undefined }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a valid WebID.'); request = createPostFormRequest({ email, register, webId: '' }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a valid WebID.'); }); it('errors on invalid passwords when registering.', async(): Promise => { request = createPostFormRequest({ email, webId, password, confirmPassword: 'bad', register }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Your password and confirmation did not match.'); }); it('errors on invalid pod names when required.', async(): Promise => { request = createPostFormRequest({ email, webId, createPod, podName: undefined }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please specify a Pod name.'); request = createPostFormRequest({ email, webId, createPod, podName: ' ' }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please specify a Pod name.'); request = createPostFormRequest({ email, webId, createWebId }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please specify a Pod name.'); }); it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise => { request = createPostFormRequest({ email, podName, createWebId }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a password.'); request = createPostFormRequest({ email, podName, createWebId, createPod }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a password.'); request = createPostFormRequest({ email, podName, createWebId, createPod, register }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please enter a password.'); }); it('errors when no option is chosen.', async(): Promise => { request = createPostFormRequest({ email, webId }); - await expect(handler.handle({ request, response })) + await expect(handler.handle({ request })) .rejects.toThrow('Please register for a WebID or create a Pod.'); }); }); @@ -144,7 +142,7 @@ describe('A RegistrationHandler', (): void => { describe('handling data', (): void => { it('can register a user.', async(): Promise => { request = createPostFormRequest({ email, webId, password, confirmPassword, register }); - await expect(handler.handle({ request, response })).resolves.toEqual({ + await expect(handler.handle({ request })).resolves.toEqual({ details: { email, webId, @@ -171,7 +169,7 @@ describe('A RegistrationHandler', (): void => { it('can create a pod.', async(): Promise => { const params = { email, webId, podName, createPod }; request = createPostFormRequest(params); - await expect(handler.handle({ request, response })).resolves.toEqual({ + await expect(handler.handle({ request })).resolves.toEqual({ details: { email, webId, @@ -200,7 +198,7 @@ describe('A RegistrationHandler', (): void => { const params = { email, webId, password, confirmPassword, podName, register, createPod }; podSettings.oidcIssuer = baseUrl; request = createPostFormRequest(params); - await expect(handler.handle({ request, response })).resolves.toEqual({ + await expect(handler.handle({ request })).resolves.toEqual({ details: { email, webId, @@ -232,7 +230,7 @@ describe('A RegistrationHandler', (): void => { podSettings.oidcIssuer = baseUrl; request = createPostFormRequest(params); (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error')); - await expect(handler.handle({ request, response })).rejects.toThrow('pod error'); + await expect(handler.handle({ request })).rejects.toThrow('pod error'); expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1); expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId }); @@ -255,7 +253,7 @@ describe('A RegistrationHandler', (): void => { podSettings.oidcIssuer = baseUrl; request = createPostFormRequest(params); - await expect(handler.handle({ request, response })).resolves.toEqual({ + await expect(handler.handle({ request })).resolves.toEqual({ details: { email, webId: generatedWebID, @@ -285,7 +283,7 @@ describe('A RegistrationHandler', (): void => { const params = { email, webId, podName, createPod }; request = createPostFormRequest(params); (podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error')); - const prom = handler.handle({ request, response }); + const prom = handler.handle({ request }); await expect(prom).rejects.toThrow('pod error'); await expect(prom).rejects.toThrow(IdpInteractionError); // Using the cleaned input for prefilled diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts index c6bedcd42..bd7204a1f 100644 --- a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts @@ -3,12 +3,10 @@ import { } from '../../../../../../src/identity/interaction/email-password/handler/ResetPasswordHandler'; import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore'; import type { HttpRequest } from '../../../../../../src/server/HttpRequest'; -import type { HttpResponse } from '../../../../../../src/server/HttpResponse'; import { createPostFormRequest } from './Util'; describe('A ResetPasswordHandler', (): void => { let request: HttpRequest; - const response: HttpResponse = {} as any; const recordId = '123456'; const url = `/resetURL/${recordId}`; const email = 'alice@test.email'; @@ -28,27 +26,27 @@ describe('A ResetPasswordHandler', (): void => { it('errors for non-string recordIds.', async(): Promise => { const errorMessage = 'Invalid request. Open the link from your email again'; request = createPostFormRequest({}); - await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage); + await expect(handler.handle({ request })).rejects.toThrow(errorMessage); request = createPostFormRequest({}, ''); - await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage); + await expect(handler.handle({ request })).rejects.toThrow(errorMessage); }); it('errors for invalid passwords.', async(): Promise => { const errorMessage = 'Your password and confirmation did not match.'; request = createPostFormRequest({ password: 'password!', confirmPassword: 'otherPassword!' }, url); - await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage); + await expect(handler.handle({ request })).rejects.toThrow(errorMessage); }); it('errors for invalid emails.', async(): Promise => { const errorMessage = 'This reset password link is no longer valid.'; request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); (accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined); - await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage); + await expect(handler.handle({ request })).rejects.toThrow(errorMessage); }); it('renders a message on success.', async(): Promise => { request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); - await expect(handler.handle({ request, response })).resolves.toEqual({ type: 'response' }); + await expect(handler.handle({ request })).resolves.toEqual({ type: 'response' }); expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1); expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId); expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1); @@ -61,6 +59,6 @@ describe('A ResetPasswordHandler', (): void => { const errorMessage = 'Unknown error: not native'; request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); (accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native'); - await expect(handler.handle({ request, response })).rejects.toThrow(errorMessage); + await expect(handler.handle({ request })).rejects.toThrow(errorMessage); }); });