From 168b3a45b869040bc56e8132aba4762e4b635bfc Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 29 Feb 2024 07:53:00 -0800 Subject: [PATCH 1/2] refactor: FORMS-965 rename parameter validation code (#1298) * refactor: FORMS-965 rename parameter validation code * consistency changes for tests --- .../middleware/validateParameter.js} | 33 ++- app/src/forms/form/routes.js | 8 +- .../unit/forms/auth/middleware/params.spec.js | 227 ---------------- .../middleware/validateParameter.spec.js | 257 ++++++++++++++++++ 4 files changed, 279 insertions(+), 246 deletions(-) rename app/src/forms/{auth/middleware/params.js => common/middleware/validateParameter.js} (79%) delete mode 100644 app/tests/unit/forms/auth/middleware/params.spec.js create mode 100644 app/tests/unit/forms/common/middleware/validateParameter.spec.js diff --git a/app/src/forms/auth/middleware/params.js b/app/src/forms/common/middleware/validateParameter.js similarity index 79% rename from app/src/forms/auth/middleware/params.js rename to app/src/forms/common/middleware/validateParameter.js index 4d6b3d11e..ced072cc0 100644 --- a/app/src/forms/auth/middleware/params.js +++ b/app/src/forms/common/middleware/validateParameter.js @@ -3,6 +3,21 @@ const uuid = require('uuid'); const formService = require('../../form/service'); +/** + * Throws a 400 problem if the parameter is not a valid UUID. + * + * @param {*} parameter the parameter to validate as a UUID. + * @param {*} parameterName the name of the parameter to use in 400 Problems. + * @throws Problem if the parameter is not a valid UUID. + */ +const _validateUuid = (parameter, parameterName) => { + if (!uuid.validate(parameter)) { + throw new Problem(400, { + detail: 'Bad ' + parameterName, + }); + } +}; + /** * Validates that the :formId route parameter exists and is a UUID. * @@ -13,11 +28,7 @@ const formService = require('../../form/service'); */ const validateFormId = async (_req, _res, next, formId) => { try { - if (!uuid.validate(formId)) { - throw new Problem(400, { - detail: 'Bad formId', - }); - } + _validateUuid(formId, 'formId'); next(); } catch (error) { @@ -36,11 +47,7 @@ const validateFormId = async (_req, _res, next, formId) => { */ const validateFormVersionDraftId = async (req, _res, next, formVersionDraftId) => { try { - if (!uuid.validate(formVersionDraftId)) { - throw new Problem(400, { - detail: 'Bad formVersionDraftId', - }); - } + _validateUuid(formVersionDraftId, 'formVersionDraftId'); const formVersionDraft = await formService.readDraft(formVersionDraftId); if (!formVersionDraft || formVersionDraft.formId !== req.params.formId) { @@ -66,11 +73,7 @@ const validateFormVersionDraftId = async (req, _res, next, formVersionDraftId) = */ const validateFormVersionId = async (req, _res, next, formVersionId) => { try { - if (!uuid.validate(formVersionId)) { - throw new Problem(400, { - detail: 'Bad formVersionId', - }); - } + _validateUuid(formVersionId, 'formVersionId'); const formVersion = await formService.readVersion(formVersionId); if (!formVersion || formVersion.formId !== req.params.formId) { diff --git a/app/src/forms/form/routes.js b/app/src/forms/form/routes.js index 895b10aff..4da2cbb56 100644 --- a/app/src/forms/form/routes.js +++ b/app/src/forms/form/routes.js @@ -2,7 +2,7 @@ const config = require('config'); const routes = require('express').Router(); const apiAccess = require('../auth/middleware/apiAccess'); const { currentUser, hasFormPermissions } = require('../auth/middleware/userAccess'); -const params = require('../auth/middleware/params'); +const validateParameter = require('../common/middleware/validateParameter'); const P = require('../common/constants').Permissions; const rateLimiter = require('../common/middleware').apiKeyRateLimiter; @@ -11,9 +11,9 @@ const controller = require('./controller'); routes.use(currentUser); -routes.param('formId', params.validateFormId); -routes.param('formVersionDraftId', params.validateFormVersionDraftId); -routes.param('formVersionId', params.validateFormVersionId); +routes.param('formId', validateParameter.validateFormId); +routes.param('formVersionDraftId', validateParameter.validateFormVersionDraftId); +routes.param('formVersionId', validateParameter.validateFormVersionId); routes.get('/', keycloak.protect(`${config.get('server.keycloak.clientId')}:admin`), async (req, res, next) => { await controller.listForms(req, res, next); diff --git a/app/tests/unit/forms/auth/middleware/params.spec.js b/app/tests/unit/forms/auth/middleware/params.spec.js deleted file mode 100644 index 0de4a7d2f..000000000 --- a/app/tests/unit/forms/auth/middleware/params.spec.js +++ /dev/null @@ -1,227 +0,0 @@ -const { getMockReq, getMockRes } = require('@jest-mock/express'); -const { v4: uuidv4 } = require('uuid'); - -const params = require('../../../../../src/forms/auth/middleware/params'); -const formService = require('../../../../../src/forms/form/service'); - -const formId = uuidv4(); - -// Various types of invalid UUIDs that we see in API calls. -const invalidUuids = [[''], ['undefined'], ['{{id}}'], ['${id}'], [uuidv4() + '.'], [' ' + uuidv4() + ' ']]; - -afterEach(() => { - jest.clearAllMocks(); -}); - -describe('validateFormId', () => { - it('400s if the formId is missing', async () => { - const req = getMockReq({ - params: {}, - }); - const { res, next } = getMockRes(); - - await params.validateFormId(req, res, next); - - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - test.each(invalidUuids)('400s if the formId is "%s"', async (eachFormId) => { - const req = getMockReq({ - params: { formId: eachFormId }, - }); - const { res, next } = getMockRes(); - - await params.validateFormId(req, res, next, eachFormId); - - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - it('passes through if the formId is valid', async () => { - const req = getMockReq({ - params: { - formId: formId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormId(req, res, next, formId); - - expect(next).toHaveBeenCalledWith(); - }); -}); - -describe('validateFormVersionDraftId', () => { - const formVersionDraftId = uuidv4(); - - const mockReadDraftResponse = { - formId: formId, - id: formVersionDraftId, - }; - - formService.readDraft = jest.fn().mockReturnValue(mockReadDraftResponse); - - it('400s if the formVersionDraftId is missing', async () => { - const req = getMockReq({ - params: { - formId: formId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionDraftId(req, res, next); - - expect(formService.readDraft).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - test.each(invalidUuids)('400s if the formVersionDraftId is "%s"', async (eachFormVersionDraftId) => { - const req = getMockReq({ - params: { formId: formId, formVersionDraftId: eachFormVersionDraftId }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionDraftId(req, res, next, eachFormVersionDraftId); - - expect(formService.readDraft).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - it('404s if the formId does not match', async () => { - formService.readDraft.mockReturnValueOnce({ - formId: uuidv4(), - id: formVersionDraftId, - }); - const req = getMockReq({ - params: { - formId: formId, - formVersionDraftId: formVersionDraftId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionDraftId(req, res, next, formVersionDraftId); - - expect(formService.readDraft).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 404 })); - }); - - it('propagates service errors', async () => { - const error = new Error(); - formService.readDraft.mockRejectedValueOnce(error); - const req = getMockReq({ - params: { - formId: formId, - formVersionDraftId: formVersionDraftId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionDraftId(req, res, next, formVersionDraftId); - - expect(formService.readDraft).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(error); - }); - - it('passes through if the formVersionDraftId matches', async () => { - const req = getMockReq({ - params: { - formId: formId, - formVersionDraftId: formVersionDraftId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionDraftId(req, res, next, formVersionDraftId); - - expect(formService.readDraft).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(); - }); -}); - -describe('validateFormVersionId', () => { - const formVersionId = uuidv4(); - - const mockReadVersionResponse = { - formId: formId, - id: formVersionId, - }; - - formService.readVersion = jest.fn().mockReturnValue(mockReadVersionResponse); - - it('400s if the formVersionId is missing', async () => { - const req = getMockReq({ - params: { - formId: formId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionId(req, res, next); - - expect(formService.readVersion).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - test.each(invalidUuids)('400s if the formVersionId is "%s"', async (eachFormVersionId) => { - const req = getMockReq({ - params: { formId: formId, formVersionId: eachFormVersionId }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionId(req, res, next, eachFormVersionId); - - expect(formService.readVersion).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); - - it('404s if the formId does not match', async () => { - formService.readVersion.mockReturnValueOnce({ - formId: uuidv4(), - id: formVersionId, - }); - const req = getMockReq({ - params: { - formId: formId, - formVersionId: formVersionId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionId(req, res, next, formVersionId); - - expect(formService.readVersion).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 404 })); - }); - - it('propagates service errors', async () => { - const error = new Error(); - formService.readVersion.mockRejectedValueOnce(error); - const req = getMockReq({ - params: { - formId: formId, - formVersionId: formVersionId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionId(req, res, next, formVersionId); - - expect(formService.readVersion).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(error); - }); - - it('passes through if the formVersionId matches', async () => { - const req = getMockReq({ - params: { - formId: formId, - formVersionId: formVersionId, - }, - }); - const { res, next } = getMockRes(); - - await params.validateFormVersionId(req, res, next, formVersionId); - - expect(formService.readVersion).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(); - }); -}); diff --git a/app/tests/unit/forms/common/middleware/validateParameter.spec.js b/app/tests/unit/forms/common/middleware/validateParameter.spec.js new file mode 100644 index 000000000..adc94ef6e --- /dev/null +++ b/app/tests/unit/forms/common/middleware/validateParameter.spec.js @@ -0,0 +1,257 @@ +const { getMockReq, getMockRes } = require('@jest-mock/express'); +const { v4: uuidv4 } = require('uuid'); + +const validateParameter = require('../../../../../src/forms/common/middleware/validateParameter'); +const formService = require('../../../../../src/forms/form/service'); + +const formId = uuidv4(); + +// Various types of invalid UUIDs that we see in API calls. +const invalidUuids = [[''], ['undefined'], ['{{id}}'], ['${id}'], [uuidv4() + '.'], [' ' + uuidv4() + ' ']]; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('validateFormId', () => { + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('formId is missing', async () => { + const req = getMockReq({ + params: {}, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormId(req, res, next); + + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test.each(invalidUuids)('formId is "%s"', async (eachFormId) => { + const req = getMockReq({ + params: { formId: eachFormId }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormId(req, res, next, eachFormId); + + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + }); + + describe('allows', () => { + test('uuid for formId', async () => { + const req = getMockReq({ + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormId(req, res, next, formId); + + expect(next).toHaveBeenCalledWith(); + }); + }); +}); + +describe('validateFormVersionDraftId', () => { + const formVersionDraftId = uuidv4(); + + const mockReadDraftResponse = { + formId: formId, + id: formVersionDraftId, + }; + + formService.readDraft = jest.fn().mockReturnValue(mockReadDraftResponse); + + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('formVersionDraftId is missing', async () => { + const req = getMockReq({ + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionDraftId(req, res, next); + + expect(formService.readDraft).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test.each(invalidUuids)('formVersionDraftId is "%s"', async (eachFormVersionDraftId) => { + const req = getMockReq({ + params: { formId: formId, formVersionDraftId: eachFormVersionDraftId }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionDraftId(req, res, next, eachFormVersionDraftId); + + expect(formService.readDraft).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + }); + + describe('404 response when', () => { + const expectedStatus = { status: 404 }; + + test('formId does not match', async () => { + formService.readDraft.mockReturnValueOnce({ + formId: uuidv4(), + id: formVersionDraftId, + }); + const req = getMockReq({ + params: { + formId: formId, + formVersionDraftId: formVersionDraftId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionDraftId(req, res, next, formVersionDraftId); + + expect(formService.readDraft).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + }); + + describe('handles error thrown by', () => { + test('readDraft', async () => { + const error = new Error(); + formService.readDraft.mockRejectedValueOnce(error); + const req = getMockReq({ + params: { + formId: formId, + formVersionDraftId: formVersionDraftId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionDraftId(req, res, next, formVersionDraftId); + + expect(formService.readDraft).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(error); + }); + }); + + describe('allows', () => { + test('form version draft with matching form id', async () => { + const req = getMockReq({ + params: { + formId: formId, + formVersionDraftId: formVersionDraftId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionDraftId(req, res, next, formVersionDraftId); + + expect(formService.readDraft).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(); + }); + }); +}); + +describe('validateFormVersionId', () => { + const formVersionId = uuidv4(); + + const mockReadVersionResponse = { + formId: formId, + id: formVersionId, + }; + + formService.readVersion = jest.fn().mockReturnValue(mockReadVersionResponse); + + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('formVersionId is missing', async () => { + const req = getMockReq({ + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionId(req, res, next); + + expect(formService.readVersion).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test.each(invalidUuids)('formVersionId is "%s"', async (eachFormVersionId) => { + const req = getMockReq({ + params: { formId: formId, formVersionId: eachFormVersionId }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionId(req, res, next, eachFormVersionId); + + expect(formService.readVersion).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + }); + + describe('404 response when', () => { + const expectedStatus = { status: 404 }; + + test('formId does not match', async () => { + formService.readVersion.mockReturnValueOnce({ + formId: uuidv4(), + id: formVersionId, + }); + const req = getMockReq({ + params: { + formId: formId, + formVersionId: formVersionId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionId(req, res, next, formVersionId); + + expect(formService.readVersion).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + }); + + describe('handles error thrown by', () => { + test('readVersion', async () => { + const error = new Error(); + formService.readVersion.mockRejectedValueOnce(error); + const req = getMockReq({ + params: { + formId: formId, + formVersionId: formVersionId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionId(req, res, next, formVersionId); + + expect(formService.readVersion).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(error); + }); + }); + + describe('allows', () => { + test('form version with matching form id', async () => { + const req = getMockReq({ + params: { + formId: formId, + formVersionId: formVersionId, + }, + }); + const { res, next } = getMockRes(); + + await validateParameter.validateFormVersionId(req, res, next, formVersionId); + + expect(formService.readVersion).toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(); + }); + }); +}); From 9d345a29b9a50cd472e4f5d42eaa4b2f4bd48364 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 29 Feb 2024 07:57:09 -0800 Subject: [PATCH 2/2] test: FORMS-914 rate limiter unit tests (#1300) * test: FORMS-914 rate limiter unit tests * Undid unnecessary change * switched rate limit strings to template literals * changed if clause formatting for consistency * tests missing async keyword --- .../forms/common/middleware/rateLimiter.js | 4 +- .../common/middleware/rateLimiter.spec.js | 144 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 app/tests/unit/forms/common/middleware/rateLimiter.spec.js diff --git a/app/src/forms/common/middleware/rateLimiter.js b/app/src/forms/common/middleware/rateLimiter.js index ed673918b..b31d9ec69 100644 --- a/app/src/forms/common/middleware/rateLimiter.js +++ b/app/src/forms/common/middleware/rateLimiter.js @@ -7,8 +7,8 @@ const apiKeyRateLimiter = rateLimit({ limit: config.get('server.rateLimit.public.max'), - // Skip Bearer token auth so that CHEFS app users are not limited. - skip: (req) => req.headers && req.headers.authorization && !req.headers.authorization.startsWith('Basic '), + // Skip everything except Basic auth so that CHEFS app users are not limited. + skip: (req) => !req.headers?.authorization || !req.headers.authorization.startsWith('Basic '), // Use the latest draft of the IETF standard for rate limiting headers. standardHeaders: 'draft-7', diff --git a/app/tests/unit/forms/common/middleware/rateLimiter.spec.js b/app/tests/unit/forms/common/middleware/rateLimiter.spec.js new file mode 100644 index 000000000..001daebfc --- /dev/null +++ b/app/tests/unit/forms/common/middleware/rateLimiter.spec.js @@ -0,0 +1,144 @@ +const { getMockReq, getMockRes } = require('@jest-mock/express'); +const uuid = require('uuid'); + +const { apiKeyRateLimiter } = require('../../../../../src/forms/common/middleware'); + +const rateLimit = 7; +const rateWindowSeconds = 11; + +jest.mock('config', () => { + return { + get: jest.fn((key) => { + if (key === 'server.rateLimit.public.max') { + return rateLimit; + } + + if (key === 'server.rateLimit.public.windowMs') { + return rateWindowSeconds * 1000; + } + }), + }; +}); + +// Headers for Draft 7 of the standard. +const rateLimitName = 'RateLimit'; +const rateLimitValue = `limit=${rateLimit}, remaining=${rateLimit - 1}, reset=${rateWindowSeconds}`; +const rateLimitPolicyName = 'RateLimit-Policy'; +const rateLimitPolicyValue = `${rateLimit};w=${rateWindowSeconds}`; + +const ipAddress = '1.2.3.4'; + +const formId = uuid.v4(); +const secret = uuid.v4(); +const basicToken = Buffer.from(`${formId}:${secret}`).toString('base64'); + +const bearerToken = Math.random().toString(36).substring(2); + +beforeEach(() => { + // Reset the rate limiting to be able to call the rate limiter multiple times + apiKeyRateLimiter.resetKey(ipAddress); +}); + +describe('apiKeyRateLimiter', () => { + it('rate limits basic auth', async () => { + const req = getMockReq({ + headers: { + authorization: 'Basic ' + basicToken, + }, + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(2); + // These also test that the rate limiter uses our custom config values. + expect(res.setHeader).toHaveBeenNthCalledWith(1, rateLimitPolicyName, rateLimitPolicyValue); + expect(res.setHeader).toHaveBeenNthCalledWith(2, rateLimitName, rateLimitValue); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + describe('skips rate limiting for', () => { + test('no headers', async () => { + const req = getMockReq({ + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('no authorization header', async () => { + const req = getMockReq({ + headers: {}, + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('empty authorization header', async () => { + const req = getMockReq({ + headers: { + authorization: '', + }, + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('unexpected authorization type', async () => { + const req = getMockReq({ + headers: { + authorization: Math.random().toString(36).substring(2), + }, + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('bearer auth', async () => { + const req = getMockReq({ + headers: { + authorization: 'Bearer ' + bearerToken, + }, + ip: ipAddress, + }); + req.app.get = jest.fn().mockReturnValue(); + const { res, next } = getMockRes(); + + await apiKeyRateLimiter(req, res, next); + + expect(res.setHeader).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + }); +});