diff --git a/app.js b/app.js
index f269ab333..3a9d61684 100644
--- a/app.js
+++ b/app.js
@@ -1,27 +1,28 @@
-const enableAppInsights = require('./app/app-insights/app-insights');
+import enableAppInsights from './app/app-insights/app-insights.js';
+import payloadGuard from './app/service/service-payloadGuard.js';
+
+import express from 'express';
+import cookieParser from 'cookie-parser';
+import { legacyCreateProxyMiddleware as proxy } from 'http-proxy-middleware';
+import config from 'config';
+import loggingPkg from '@hmcts/nodejs-logging';
+const { Express: ExpressLogger, Logger } = loggingPkg;
+import {authCheckerUserOnlyFilter, mapFetchErrors} from './app/user/auth-checker-user-only-filter.js';
+import addressLookup from './app/address/address-lookup.js';
+import serviceFilter from './app/service/service-filter.js';
+import corsHandler from './app/security/cors.js';
+import handleTiming from './app/security/timing.js';
+import hstsHandler from './app/security/hsts.js';
+import healthcheck from '@hmcts/nodejs-healthcheck';
+import routes from '@hmcts/nodejs-healthcheck/healthcheck/routes.js';
+import {oauth2Route} from './app/oauth2/oauth2-route.js';
+import {logoutRoute} from './app/oauth2/logout-route.js';
+import noCache from 'nocache';
+import noSniff from 'dont-sniff-mimetype';
enableAppInsights();
-let express = require('express');
-let cookieParser = require('cookie-parser');
-const { legacyCreateProxyMiddleware: proxy } = require('http-proxy-middleware');
-const config = require('config');
-const { Express: ExpressLogger, Logger } = require('@hmcts/nodejs-logging');
-const {authCheckerUserOnlyFilter} = require('./app/user/auth-checker-user-only-filter');
-const {mapFetchErrors} = require('./app/user/auth-checker-user-only-filter');
-const addressLookup = require('./app/address/address-lookup');
-const serviceFilter = require('./app/service/service-filter');
-const corsHandler = require('./app/security/cors');
-const handleTiming = require('./app/security/timing');
-const hstsHandler = require('./app/security/hsts');
-const healthcheck = require('@hmcts/nodejs-healthcheck');
-const routes = require('@hmcts/nodejs-healthcheck/healthcheck/routes');
-const oauth2Route = require('./app/oauth2/oauth2-route').oauth2Route;
-const logoutRoute = require('./app/oauth2/logout-route').logoutRoute;
-const noCache = require('nocache');
-const noSniff = require('dont-sniff-mimetype');
-
-let app = express();
+const app = express();
const appHealth = express();
const logger = Logger.getLogger('app');
@@ -77,6 +78,18 @@ app.get('/logout', logoutRoute);
app.use(serviceFilter);
+// parsing + basic size limits (protects DoS)
+app.use('/data', express.json({ limit: '1mb', strict: true }));
+app.use('/data', express.urlencoded({ limit: '1mb', extended: false }));
+
+// lightweight safety/validation middleware
+app.use('/data', payloadGuard({
+ allowContentTypes: ['application/json'],
+ maxArrayLength: 10000, // sanity cap
+ rejectPathTraversal: true,
+ rejectObviousScriptTags: true
+}));
+
app.get('/addresses',(req, res, next) => {
addressLookup(req.query.postcode)
.then(result => res.send(result))
@@ -163,7 +176,7 @@ app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
- let status = isNaN(err.status) ? 500 : err.status;
+ let status = Number.isNaN(err.status) ? 500 : err.status;
res.status(status);
res.json({
error: err.error || 'Unauthorized',
@@ -172,4 +185,4 @@ app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars
});
});
-module.exports = app;
+export default app;
diff --git a/app/address/address-lookup.js b/app/address/address-lookup.js
index e8bbd2f0e..8ad3d23a3 100644
--- a/app/address/address-lookup.js
+++ b/app/address/address-lookup.js
@@ -1,9 +1,10 @@
-const config = require('config');
-const fetch = require('node-fetch').default;
-const { HttpsProxyAgent } = require('https-proxy-agent');
-const { Logger } = require('@hmcts/nodejs-logging');
+import config from 'config';
+import fetch from 'node-fetch';
+import {HttpsProxyAgent} from 'https-proxy-agent';
+import {Logger} from '@hmcts/nodejs-logging';
+
const logger = Logger.getLogger('addressLookup');
-const crypto = require('crypto');
+import crypto from 'crypto';
function addressLookup(postcode) {
@@ -50,4 +51,4 @@ function badResponse(res, body) {
throw errorObj(`An error response was received from the Postcode Service. Status=${res.status}, Body=${body}`, res.status);
}
-module.exports = addressLookup;
+export default addressLookup;
diff --git a/app/app-insights/app-insights.js b/app/app-insights/app-insights.js
index e8f0a26f3..701c94eb9 100644
--- a/app/app-insights/app-insights.js
+++ b/app/app-insights/app-insights.js
@@ -1,7 +1,5 @@
-const config = require('config');
-const appInsights = require('applicationinsights');
-
-const enabled = config.get('appInsights.enabled');
+import config from 'config';
+import appInsights from 'applicationinsights';
function fineGrainedSampling(envelope) {
// activity data is not interesting and should not really be going through this proxy anyway
@@ -9,11 +7,11 @@ function fineGrainedSampling(envelope) {
if (['RequestData', 'RemoteDependencyData'].includes(envelope.data.baseType) && (envelope.data.baseData.name.includes('/activity') || envelope.data.baseData.name.includes('/health'))) {
envelope.sampleRate = 1;
}
-
return true;
}
-const enableAppInsights = () => {
+export default function enableAppInsights(){
+ const enabled = config.get('appInsights.enabled');
if (enabled) {
const appInsightsKey = config.get('secrets.ccd.AppInsightsInstrumentationKey');
const appInsightsRoleName = config.get('appInsights.roleName');
@@ -25,5 +23,3 @@ const enableAppInsights = () => {
appInsights.start();
}
};
-
-module.exports = enableAppInsights;
diff --git a/app/cache/cache-config.js b/app/cache/cache-config.js
index 72dd77506..4bca639f6 100644
--- a/app/cache/cache-config.js
+++ b/app/cache/cache-config.js
@@ -1,10 +1,17 @@
-const CacheService = require('./cache-service');
-const config = require('config');
+import config from 'config';
+import CacheService from './cache-service.js';
// TTL for userInfoCache should always be set to lower value than the actual token's TTL
// to limit risk/time of an expired token still returning details - config default is 10 mins
-const userInfoCache = new CacheService('UserInfoCache', config.get('cache.user_info_ttl'), config.get('cache.user_info_check_period'));
+let userInfoCacheInstance;
-module.exports = {
- userInfoCache
+function userInfoCache() {
+ if (!userInfoCacheInstance) {
+ userInfoCacheInstance = new CacheService('UserInfoCache', config.get('cache.user_info_ttl'), config.get('cache.user_info_check_period'));
+ }
+ return userInfoCacheInstance;
+}
+
+export {
+ userInfoCache
};
diff --git a/app/cache/cache-service.js b/app/cache/cache-service.js
index 83038b7c5..a48eb17e7 100644
--- a/app/cache/cache-service.js
+++ b/app/cache/cache-service.js
@@ -1,11 +1,12 @@
-const NodeCache = require('node-cache');
-const { Logger } = require('@hmcts/nodejs-logging');
+import NodeCache from 'node-cache';
+import {Logger} from '@hmcts/nodejs-logging';
+
const logger = Logger.getLogger('CacheService');
class Cache {
constructor(name, ttlSeconds, checkPeriodSeconds) {
- this.cache = new NodeCache({ stdTTL: ttlSeconds, checkperiod: checkPeriodSeconds });
+ this.cache = new NodeCache({stdTTL: ttlSeconds, checkperiod: checkPeriodSeconds});
this.name = name;
}
@@ -41,4 +42,4 @@ class Cache {
}
}
-module.exports = Cache;
+export default Cache;
diff --git a/app/oauth2/access-token-request.js b/app/oauth2/access-token-request.js
index 2caea7fa9..478b226e8 100644
--- a/app/oauth2/access-token-request.js
+++ b/app/oauth2/access-token-request.js
@@ -1,7 +1,7 @@
-const config = require('config');
-const fetch = require('node-fetch').default;
-const { URL } = require('url');
-const { Logger } = require('@hmcts/nodejs-logging');
+import config from 'config';
+import fetch from 'node-fetch';
+import {URL} from 'url';
+import {Logger} from '@hmcts/nodejs-logging';
const logger = Logger.getLogger('accessTokenRequest');
@@ -50,4 +50,4 @@ function accessTokenRequest(request) {
throw error;
});
}
-module.exports = accessTokenRequest;
+export default accessTokenRequest;
diff --git a/app/oauth2/logout-route.js b/app/oauth2/logout-route.js
index 31e539152..2e308f467 100644
--- a/app/oauth2/logout-route.js
+++ b/app/oauth2/logout-route.js
@@ -1,8 +1,9 @@
-const config = require('config');
-const fetch = require('node-fetch').default;
-const COOKIE_ACCESS_TOKEN = require('./oauth2-route').COOKIE_ACCESS_TOKEN;
+import config from 'config';
+import fetch from 'node-fetch';
+import {COOKIE_ACCESS_TOKEN} from './oauth2-route.js';
+
const TOKEN_PLACEHOLDER = ':token';
-const { userInfoCache } = require('../cache/cache-config');
+import {userInfoCache} from '../cache/cache-config.js';
const logoutRoute = (req, res, next) => {
const accessToken = req.cookies && req.cookies[COOKIE_ACCESS_TOKEN];
@@ -20,7 +21,7 @@ const logoutRoute = (req, res, next) => {
fetch(config.get('idam.oauth2.logout_endpoint').replace(TOKEN_PLACEHOLDER, accessToken), options)
.then(() => {
res.clearCookie(COOKIE_ACCESS_TOKEN);
- userInfoCache.del(accessToken);
+ userInfoCache().del(accessToken);
res.status(204).send();
})
.catch(err => next(err));
@@ -33,6 +34,6 @@ const logoutRoute = (req, res, next) => {
}
};
-module.exports = {
+export {
logoutRoute
};
diff --git a/app/oauth2/oauth2-route.js b/app/oauth2/oauth2-route.js
index 826ee5fc9..9a5a15045 100644
--- a/app/oauth2/oauth2-route.js
+++ b/app/oauth2/oauth2-route.js
@@ -1,26 +1,27 @@
-const accessTokenRequest = require('./access-token-request');
-const config = require('config');
+import accessTokenRequest from './access-token-request.js';
+import config from 'config';
+
const COOKIE_ACCESS_TOKEN = 'accessToken';
const oauth2Route = (req, res, next) => {
accessTokenRequest(req)
.then(result => {
- if( result.status === 200 ) {
+ if (result.status === 200) {
- result.json().then ( jsonResult => {
+ result.json().then(jsonResult => {
- res.cookie(COOKIE_ACCESS_TOKEN, jsonResult.access_token,
- {
- maxAge: jsonResult.expires_in * 1000,
- httpOnly: true,
- secure: config.get('security.secure_auth_cookie_enabled')
- });
- res.status(204).send();
+ res.cookie(COOKIE_ACCESS_TOKEN, jsonResult.access_token,
+ {
+ maxAge: jsonResult.expires_in * 1000,
+ httpOnly: true,
+ secure: config.get('security.secure_auth_cookie_enabled')
+ });
+ res.status(204).send();
}
);
} else {
next({
- status: 502,
+ status: 502,
message: 'Internal Server Error'
});
}
@@ -29,7 +30,7 @@ const oauth2Route = (req, res, next) => {
.catch(err => next(err));
};
-module.exports = {
+export {
COOKIE_ACCESS_TOKEN,
oauth2Route
};
diff --git a/app/security/cors.js b/app/security/cors.js
index 679c3676d..f5ad99c14 100644
--- a/app/security/cors.js
+++ b/app/security/cors.js
@@ -1,5 +1,5 @@
-const config = require('config');
-const sanitize = require('../util/sanitize');
+import config from 'config';
+import {sanitizeData} from '../util/sanitize.js';
const WILDCARD = '*';
@@ -23,7 +23,7 @@ const handleCors = (req, res, next) => {
if (corsOptions.allowOrigin) {
const origin = req.get('origin');
if (corsOptions.allowOrigin(origin)) {
- res.set('Access-Control-Allow-Origin', sanitize.sanitizeData(origin));
+ res.set('Access-Control-Allow-Origin', sanitizeData(origin));
}
} else {
res.set('Access-Control-Allow-Origin', '*');
@@ -34,7 +34,7 @@ const handleCors = (req, res, next) => {
if (corsOptions.allowMethods) {
res.set('Access-Control-Allow-Methods', corsOptions.allowMethods);
}
- res.set('Access-Control-Allow-Headers', sanitize.sanitizeData(req.get('Access-Control-Request-Headers')));
+ res.set('Access-Control-Allow-Headers', sanitizeData(req.get('Access-Control-Request-Headers')));
if('OPTIONS' === req.method) {
res
.status(200)
@@ -44,4 +44,4 @@ const handleCors = (req, res, next) => {
}
};
-module.exports = handleCors;
+export default handleCors;
diff --git a/app/security/hsts.js b/app/security/hsts.js
index b5e3cbbd8..8d16e20c6 100644
--- a/app/security/hsts.js
+++ b/app/security/hsts.js
@@ -3,4 +3,4 @@ const handleHSTS = (req, res, next) => {
next();
};
-module.exports = handleHSTS;
+export default handleHSTS;
diff --git a/app/security/timing.js b/app/security/timing.js
index 8a3695f27..a932d4927 100644
--- a/app/security/timing.js
+++ b/app/security/timing.js
@@ -1,8 +1,8 @@
-const config = require('config');
+import config from 'config';
const handleTiming = (req, res, next) => {
res.set('Timing-Allow-Origin', config.get('security.timing-allow-origin'));
next();
};
-module.exports = handleTiming;
+export default handleTiming;
diff --git a/app/service/service-filter.js b/app/service/service-filter.js
index 79fa38473..8cfe14689 100644
--- a/app/service/service-filter.js
+++ b/app/service/service-filter.js
@@ -1,5 +1,5 @@
-const serviceTokenGenerator = require('./service-token-generator');
-const { Logger } = require('@hmcts/nodejs-logging');
+import serviceTokenGenerator from './service-token-generator.js';
+import {Logger} from '@hmcts/nodejs-logging';
const logger = Logger.getLogger('serviceFilter');
@@ -26,4 +26,4 @@ const serviceFilter = (req, res, next) => {
});
};
-module.exports = serviceFilter;
+export default serviceFilter;
diff --git a/app/service/service-payloadGuard.js b/app/service/service-payloadGuard.js
new file mode 100644
index 000000000..36954264d
--- /dev/null
+++ b/app/service/service-payloadGuard.js
@@ -0,0 +1,115 @@
+import {Logger} from '@hmcts/nodejs-logging';
+
+function payloadGuard(opts) {
+ const logger = Logger.getLogger('payloadGuard');
+
+ const {
+ allowContentTypes = ['application/json'],
+ rejectPathTraversal = true,
+ rejectObviousScriptTags = true,
+ maxArrayLength = 10000,
+ maxDepth = 40
+ } = opts || {};
+
+ return (req, res, next) => {
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
+ const contentType = (req.headers['content-type'] || '').split(';')[0].trim();
+ if (allowContentTypes.length && !allowContentTypes.includes(contentType)) {
+ logger.warn(`Invalid content-type: ${contentType}`);
+ return res.status(415).json({
+ status: 415,
+ error: 'Unsupported Media Type'
+ });
+ }
+
+ if (rejectPathTraversal && isSuspiciousPath(req.url || '')) {
+ logger.warn(`Path traversal attempt: ${req.url || ''}`);
+ return res.status(400).json({message: 'Invalid path'});
+ }
+
+ if (req.body && typeof req.body === 'object') {
+ const seen = new Set();
+ let depth = 0;
+ let tooBig = false;
+
+ (function walk(v, d) {
+ if (tooBig || d > maxDepth) {
+ tooBig = true;
+ return;
+ }
+ if (v && typeof v === 'object') {
+ if (seen.has(v)) return; // cycles
+ seen.add(v);
+ if (Array.isArray(v) && v.length > maxArrayLength) {
+ tooBig = true;
+ return;
+ }
+ for (const k of Object.keys(v)) walk(v[k], d + 1);
+ }
+ })(req.body, depth);
+
+ if (tooBig) {
+ logger.warn('Payload too large or too deeply nested');
+ return res.status(413).json({message: 'Payload too large or too deeply nested'});
+ }
+
+ }
+
+ if (rejectObviousScriptTags) {
+ const hasScript = containsScriptTag(req.body);
+ if (hasScript) {
+ logger.warn('Payload contains disallowed content');
+ return res.status(400).json({message: 'Payload contains disallowed content'});
+ }
+ }
+ }
+ next();
+ };
+}
+
+function safeDecodeURIComponent(value) {
+ return String(value).replace(/%[0-9a-fA-F]{2}/g, m => {
+ const code = parseInt(m.slice(1), 16);
+ return String.fromCharCode(code);
+ });
+}
+
+function extractPath(rawUrl) {
+ const noHash = rawUrl.split('#', 1)[0];
+ return noHash.split('?', 1)[0];
+}
+
+function isSuspiciousPath(rawUrl) {
+ const pathname = extractPath(rawUrl);
+ const decodedRaw = safeDecodeURIComponent(rawUrl);
+ const decodedPath = safeDecodeURIComponent(pathname);
+
+ return (
+ rawUrl.includes('..') ||
+ decodedRaw.includes('..') ||
+ pathname.includes('..') ||
+ pathname.includes('\\') ||
+ decodedPath.includes('..') ||
+ decodedPath.includes('\\') ||
+ decodedRaw.includes('\\') ||
+ rawUrl.includes('\\') ||
+ /%2e|%2f|%5c/i.test(rawUrl) ||
+ /%2e|%2f|%5c/i.test(pathname)
+ );
+}
+
+function containsScriptTag(obj) {
+ const re = /<\s*script\b|javascript:/i;
+ const stack = [obj];
+ while (stack.length) {
+ const cur = stack.pop();
+ if (typeof cur === 'string') {
+ if (re.test(cur)) return true;
+ } else if (cur && typeof cur === 'object') {
+ for (const k in cur) stack.push(cur[k]);
+ }
+ }
+ return false;
+}
+
+export default payloadGuard;
diff --git a/app/service/service-token-generator.js b/app/service/service-token-generator.js
index f709834fc..38a597e15 100644
--- a/app/service/service-token-generator.js
+++ b/app/service/service-token-generator.js
@@ -1,7 +1,7 @@
-const OTP = require('otp');
-const config = require('config');
-const { jwtDecode } = require('jwt-decode');
-const fetch = require('../util/fetch');
+import config from 'config';
+import {jwtDecode} from 'jwt-decode';
+import fetch from '../util/fetch.js';
+import OTP from 'otp';
const idamS2SUrl = config.get('idam.s2s_url');
const serviceName = config.get('idam.service_name');
@@ -41,4 +41,4 @@ const serviceTokenGenerator = () => {
}
};
-module.exports = serviceTokenGenerator;
+export default serviceTokenGenerator;
diff --git a/app/user/auth-checker-user-only-filter.js b/app/user/auth-checker-user-only-filter.js
index f2d5d4fea..48d87de63 100644
--- a/app/user/auth-checker-user-only-filter.js
+++ b/app/user/auth-checker-user-only-filter.js
@@ -1,13 +1,12 @@
-const userRequestAuthorizer = require('./user-request-authorizer');
-const { Logger } = require('@hmcts/nodejs-logging');
+import {authorise} from './user-request-authorizer.js';
+import {Logger} from '@hmcts/nodejs-logging';
const logger = Logger.getLogger('authCheckerUserOnlyFilter');
const authCheckerUserOnlyFilter = (req, res, next) => {
req.authentication = {};
- userRequestAuthorizer
- .authorise(req)
+ authorise(req)
.then(user => {
req.authentication.user = user;
req.headers['user-id'] = user.uid;
@@ -27,7 +26,7 @@ const authCheckerUserOnlyFilter = (req, res, next) => {
};
const isBadGatewayError = (error) => {
- return error.message !== undefined && (error.message.includes('getaddrinfo ENOTFOUND') ||
+ return error.message !== undefined && (error.message.includes('getaddrinfo ENOTFOUND') ||
error.message.includes('socket hang up') ||
error.message.includes('getaddrinfo EAI_AGAIN') ||
error.message.includes('connect ETIMEOUT') ||
@@ -71,7 +70,7 @@ const mapFetchErrors = (error, res, next) => {
}
};
-module.exports = {
+export {
authCheckerUserOnlyFilter,
mapFetchErrors
};
diff --git a/app/user/authorised-roles-extractor.js b/app/user/authorised-roles-extractor.js
index 2ead0420f..44788a36e 100644
--- a/app/user/authorised-roles-extractor.js
+++ b/app/user/authorised-roles-extractor.js
@@ -19,4 +19,4 @@ const extract = (request) => {
return [];
};
-exports.extract = extract;
+export default extract;
diff --git a/app/user/cached-user-resolver.js b/app/user/cached-user-resolver.js
index 46786dbe8..083d30d7f 100644
--- a/app/user/cached-user-resolver.js
+++ b/app/user/cached-user-resolver.js
@@ -1,10 +1,10 @@
-const userResolver = require('./user-resolver');
-const { userInfoCache } = require('../cache/cache-config');
-const jwtUtil = require('../util/jwt');
+import {getUserDetails} from './user-resolver.js';
+import {userInfoCache} from '../cache/cache-config.js';
+import {removeBearer} from '../util/jwt.js';
const getCachedUserDetails = (jwt) => {
- return userInfoCache.getOrElseUpdate(jwtUtil.removeBearer(jwt),
- () => userResolver.getUserDetails(jwt));
+ return userInfoCache().getOrElseUpdate(removeBearer(jwt),
+ () => getUserDetails(jwt));
};
-exports.getUserDetails = getCachedUserDetails;
+export {getCachedUserDetails};
diff --git a/app/user/user-request-authorizer.js b/app/user/user-request-authorizer.js
index f4e241a27..854473465 100644
--- a/app/user/user-request-authorizer.js
+++ b/app/user/user-request-authorizer.js
@@ -1,9 +1,13 @@
-const authorizedRolesExtractor = require('./authorised-roles-extractor');
-const COOKIE_ACCESS_TOKEN = require('../oauth2/oauth2-route').COOKIE_ACCESS_TOKEN;
-const config = require('config');
+import extract from './authorised-roles-extractor.js';
+import {COOKIE_ACCESS_TOKEN} from '../oauth2/oauth2-route.js';
+import config from 'config';
+
const userResolver = config.get('cache.user_info_enabled')
- ? require('./cached-user-resolver')
- : require('./user-resolver');
+ ? await import('./cached-user-resolver.js')
+ : await import('./user-resolver.js');
+
+const getUserDetails = userResolver.getUserDetails
+ || userResolver.default?.getUserDetails;
const AUTHORIZATION = 'Authorization';
@@ -50,14 +54,13 @@ const authorise = (request) => {
// Use AccessToken cookie as Authorization header
if (!request.get(AUTHORIZATION) && bearerToken) {
if (!request.headers) {
- request.headers = { [AUTHORIZATION]: `Bearer ${bearerToken}` };
+ request.headers = {[AUTHORIZATION]: `Bearer ${bearerToken}`};
} else {
request.headers[AUTHORIZATION] = `Bearer ${bearerToken}`;
}
}
- return userResolver
- .getUserDetails(bearerToken)
+ return getUserDetails(bearerToken)
.then(userDetails => user = userDetails)
.then(() => fillInUserId(request, user))
.then(() => authorizeRoles(request, user))
@@ -66,7 +69,7 @@ const authorise = (request) => {
const authorizeRoles = (request, user) => {
if (request.originalUrl.includes('/caseworkers/')) {
- const roles = authorizedRolesExtractor.extract(request);
+ const roles = extract(request);
if (roles.length === 0 ||
!roles.some(role => user.roles.includes(role))) {
@@ -97,8 +100,10 @@ const fillInUserId = (request, user) => {
request.originalUrl = request.originalUrl.replace(USER_ID_PLACEHOLDER, user.uid);
};
-exports.ERROR_TOKEN_MISSING = ERROR_TOKEN_MISSING;
-exports.ERROR_UNAUTHORISED_ROLE = ERROR_UNAUTHORISED_ROLE;
-exports.ERROR_UNAUTHORISED_USER_ID = ERROR_UNAUTHORISED_USER_ID;
-exports.AUTHORIZATION = AUTHORIZATION;
-exports.authorise = authorise;
+export {
+ ERROR_TOKEN_MISSING,
+ ERROR_UNAUTHORISED_ROLE,
+ ERROR_UNAUTHORISED_USER_ID,
+ AUTHORIZATION,
+ authorise
+};
diff --git a/app/user/user-resolver.js b/app/user/user-resolver.js
index bc35781ba..31b885986 100644
--- a/app/user/user-resolver.js
+++ b/app/user/user-resolver.js
@@ -1,14 +1,14 @@
-const config = require('config');
-const fetch = require('../util/fetch');
-const jwtUtil = require('../util/jwt');
+import config from 'config';
+import fetch from '../util/fetch.js';
+import {addBearer} from '../util/jwt.js';
const getUserDetails = (jwt) => {
return fetch(`${config.get('idam.base_url')}/o/userinfo`, {
headers: {
- 'Authorization': jwtUtil.addBearer(jwt)
+ 'Authorization': addBearer(jwt)
}
})
.then(res => res.json());
};
-exports.getUserDetails = getUserDetails;
+export {getUserDetails};
diff --git a/app/util/fetch.js b/app/util/fetch.js
index 88eb39189..db25d5e73 100644
--- a/app/util/fetch.js
+++ b/app/util/fetch.js
@@ -1,4 +1,4 @@
-let _fetch = require('node-fetch').default;
+import {default as _fetch} from 'node-fetch';
const fetch = (...args) => {
return _fetch(...args)
@@ -12,4 +12,4 @@ const fetch = (...args) => {
});
};
-module.exports = fetch;
+export default fetch;
diff --git a/app/util/jwt.js b/app/util/jwt.js
index 54dbdd0e0..2d8f490a4 100644
--- a/app/util/jwt.js
+++ b/app/util/jwt.js
@@ -8,5 +8,4 @@ const removeBearer = (jwt) => {
return jwt.startsWith(BEARER_PREFIX) ? jwt.replace(BEARER_PREFIX, '') : jwt;
};
-exports.addBearer = addBearer;
-exports.removeBearer = removeBearer;
+export {addBearer, removeBearer};
diff --git a/app/util/sanitize.js b/app/util/sanitize.js
index be753e4c3..2004c95a4 100644
--- a/app/util/sanitize.js
+++ b/app/util/sanitize.js
@@ -8,4 +8,5 @@ const sanitizeData = (data) => {
return sanitizedData;
};
-exports.sanitizeData = sanitizeData;
+export {sanitizeData};
+
diff --git a/config/test.yaml b/config/test.yaml
index 4c179b46b..47396a4c1 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -2,6 +2,8 @@ appInsights:
enabled: false
idam:
base_url: http://test-idam:1234
+ cache:
+ user_info_enabled: false
secrets:
ccd:
ccd-api-gateway-oauth2-client-secret: ccd_gateway_secret
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 525c61347..17738e01a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -46,7 +46,7 @@ export default defineConfig([
codecept_helper: true,
},
- ecmaVersion: 2019,
+ ecmaVersion: 2022,
sourceType: "module",
},
diff --git a/package.json b/package.json
index d21131663..3bc79e2e2 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,9 @@
"setup": "cross-env NODE_PATH=. yarn node --version",
"lint": "NODE_PATH=. eslint --ignore-pattern coverage .",
"test": "yarn test:unit",
- "test:unit": "NODE_ENV=test NODE_PATH=. mocha 'test/**/*.spec.js' --reporter mochawesome --reporter-options reportFilename=unit,inlineAssets=true,reportTitle=ccd-api-gateway --recursive",
+ "test:unit": "NODE_ENV=test NODE_PATH=. mocha --loader=esmock 'test/**/*.spec.js' --reporter mochawesome --reporter-options reportFilename=unit,inlineAssets=true,reportTitle=ccd-api-gateway --recursive",
"test:specific": "NODE_ENV=test LOG_LEVEL=DEBUG NODE_PATH=. mocha --recursive ",
- "test:coverage": "NODE_ENV=test NODE_PATH=. nyc mocha 'test/**/*.spec.js' --recursive",
+ "test:coverage": "NODE_ENV=test NODE_PATH=. c8 mocha 'test/**/*.spec.js' --recursive",
"fortifyScan": "./test/java/gradlew -p test/java fortifyScan",
"test:smoke": "echo SMOKE TESTS NOT CONFIGURED",
"test:functional": "echo 'TODO - FUNCTIONAL TEST'",
@@ -54,10 +54,12 @@
"otp": "^0.1.3"
},
"devDependencies": {
+ "c8": "^11.0.0",
"chai": "^6.2.2",
"chai-http": "^5.1.2",
"eslint": "^10.4.1",
"eslint-plugin-mocha": "^11.3.0",
+ "esmock": "^2.7.6",
"fetch-mock": "^12.6.0",
"git-message": "^2.0.2",
"istanbul": "^0.4.5",
@@ -76,12 +78,10 @@
"sonar-scanner": "^3.1.0",
"supertest": "^7.2.2"
},
- "nyc": {
- "extension": [
- ".js"
- ],
+ "type": "module",
+ "c8": {
"include": [
- "app"
+ "app/**/*.js"
],
"reporter": [
"lcov",
@@ -90,5 +90,8 @@
"report-dir": "coverage",
"all": true
},
+ "resolutions": {
+ "@opentelemetry/core": "2.8.0"
+ },
"packageManager": "yarn@4.17.0"
}
diff --git a/server.js b/server.js
index a05bf3834..1f0238346 100644
--- a/server.js
+++ b/server.js
@@ -3,22 +3,29 @@
/**
* Module dependencies.
*/
+import config from 'config';
+import * as propertiesVolume from '@hmcts/properties-volume';
+import debugLib from 'debug';
+import http from 'node:http';
+import https from 'node:https';
+import path from 'node:path';
+import fs from 'node:fs';
+import log from '@hmcts/nodejs-logging';
+import { fileURLToPath } from 'node:url';
+
+const debug = debugLib('ccd-api-gateway-web:server');
+const logger = log.Logger.getLogger('server');
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
-require('@hmcts/properties-volume').addTo(require('config'));
-let app = require('./app');
-let debug = require('debug')('ccd-api-gateway-web:server');
-let http = require('http');
-let https = require('https');
-let path = require('path');
-let fs = require('fs');
-let log = require('@hmcts/nodejs-logging');
-const logger = log.Logger.getLogger('server');
+propertiesVolume.addTo(config);
+const { default: app } = await import('./app.js');
+
/**
* Get port from environment and store in Express.
*/
-
-let port = normalizePort(process.env.PORT || '3453');
+const port = normalizePort(process.env.PORT || '3453');
logger.info('Starting on port ' + port);
app.set('port', port);
@@ -26,7 +33,7 @@ app.set('port', port);
* Create HTTPS or HTTP server, depending on whether this is a local dev environment or not.
*/
-let server = createServer(app);
+const server = createServer(app);
function createServer(app) {
if (process.env.ENV === 'localdev') {
@@ -56,9 +63,9 @@ server.on('listening', onListening);
*/
function normalizePort(val) {
- let port = parseInt(val, 10);
+ const port = Number.parseInt(val, 10);
- if (isNaN(port)) {
+ if (Number.isNaN(port)) {
// named pipe
return val;
}
@@ -80,7 +87,7 @@ function onError(error) {
throw error;
}
- let bind = typeof port === 'string'
+ const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
@@ -104,8 +111,8 @@ function onError(error) {
*/
function onListening() {
- let addr = server.address();
- let bind = typeof addr === 'string'
+ const addr = server.address();
+ const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
diff --git a/test/address/address-lookup.spec.js b/test/address/address-lookup.spec.js
index 5346bf1ae..cdd128594 100644
--- a/test/address/address-lookup.spec.js
+++ b/test/address/address-lookup.spec.js
@@ -1,16 +1,11 @@
-const chai = require('chai');
-const expect = chai.expect;
-const nock = require('nock');
+import {expect} from 'chai';
+import nock from 'nock';
+import addressLookup from '../../app/address/address-lookup.js';
/* The line below turns off ESLints 'no-undef' for the chai 'fail' function */
/*global fail */
describe('Address Lookup', () => {
- let addressLookup;
-
- before(function(){
- addressLookup = require('../../app/address/address-lookup');
- });
it('Should expect key and postcode placed holders to be substituted', (done) => {
diff --git a/test/app-insights/app-insights.spec.js b/test/app-insights/app-insights.spec.js
index c071f40f2..6c1d99e88 100644
--- a/test/app-insights/app-insights.spec.js
+++ b/test/app-insights/app-insights.spec.js
@@ -1,6 +1,5 @@
-const chai = require('chai');
-const expect = chai.expect;
-const enableAppInsights = require('../../app/app-insights/app-insights');
+import {expect} from 'chai';
+import enableAppInsights from '../../app/app-insights/app-insights.js';
describe('Application insights', () => {
it('should initialize properly', () => {
diff --git a/test/cache/cache-service.spec.js b/test/cache/cache-service.spec.js
index 0cbcfb3f3..9d05a61b6 100644
--- a/test/cache/cache-service.spec.js
+++ b/test/cache/cache-service.spec.js
@@ -1,12 +1,14 @@
-const chai = require('chai');
-const expect = chai.expect;
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
+import {expect} from 'chai';
+import * as chai from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+
const assert = sinon.assert;
-const sinonChai = require('sinon-chai').default;
+import sinonChai from 'sinon-chai';
+
chai.use(sinonChai);
-const CacheService = require('../../app/cache/cache-service');
-const NodeCache = require('node-cache');
+import CacheService from '../../app/cache/cache-service.js';
+import NodeCache from 'node-cache';
describe('CacheService', () => {
const KEY = 'Key';
@@ -29,7 +31,7 @@ describe('CacheService', () => {
let cache;
- beforeEach(() => {
+ beforeEach(async () => {
getStub = sinon.stub();
setStub = sinon.stub();
deleteStub = sinon.stub();
@@ -40,10 +42,12 @@ describe('CacheService', () => {
del: deleteStub
};
- nodeCacheSpy = sinon.spy(function () { return MockNodeCache; });
+ nodeCacheSpy = sinon.spy(function () {
+ return MockNodeCache;
+ });
- StubbedCacheService = proxyquire('../../app/cache/cache-service', { 'node-cache': nodeCacheSpy });
- cache = new StubbedCacheService('TestCache', CACHE_TTL_SECONDS, 120);
+ StubbedCacheService = await esmock('../../app/cache/cache-service', {'node-cache': nodeCacheSpy});
+ cache = new StubbedCacheService('TestCache', CACHE_TTL_SECONDS, 120);
});
it('should get existing cache value', async () => {
diff --git a/test/health/health-check.spec.js b/test/health/health-check.spec.js
index 84e420bde..8581981c3 100644
--- a/test/health/health-check.spec.js
+++ b/test/health/health-check.spec.js
@@ -1,7 +1,6 @@
-const chai = require('chai');
-const expect = chai.expect;
-const request = require('supertest');
-const app = require('app');
+import {expect} from 'chai';
+import request from 'supertest';
+import app from '../../app.js';
describe('health check', () => {
@@ -25,19 +24,19 @@ describe('health check', () => {
it('should return 200 OK for liveness health check', async () => {
await request(app)
- .get('/health/liveness')
- .expect(res => {
- expect(res.status).equal(200);
- expect(res.body.status).equal('UP');
- });
+ .get('/health/liveness')
+ .expect(res => {
+ expect(res.status).equal(200);
+ expect(res.body.status).equal('UP');
+ });
});
it('should return 200 OK for readiness health check', async () => {
await request(app)
- .get('/health/readiness')
- .expect(res => {
- expect(res.status).equal(200);
- expect(res.body.status).equal('UP');
- });
+ .get('/health/readiness')
+ .expect(res => {
+ expect(res.status).equal(200);
+ expect(res.body.status).equal('UP');
+ });
});
});
diff --git a/test/oauth2/access-token-request.spec.js b/test/oauth2/access-token-request.spec.js
index 7a31ed620..7581134bd 100644
--- a/test/oauth2/access-token-request.spec.js
+++ b/test/oauth2/access-token-request.spec.js
@@ -1,11 +1,11 @@
-const chai = require('chai');
-const expect = chai.expect;
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
+import * as chai from 'chai';
+import {expect} from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
const assert = sinon.assert;
-const sinonChai = require('sinon-chai').default;
-const sinonExpressMock = require('sinon-express-mock');
-const url = require('url');
+import sinonChai from 'sinon-chai';
+import sinonExpressMock from 'sinon-express-mock';
+import url from 'url';
chai.use(sinonChai);
describe('Access Token Request', () => {
@@ -58,35 +58,35 @@ describe('Access Token Request', () => {
let accessTokenRequest;
let unsuccessfulAccessTokenRequest;
- beforeEach(() => {
+ beforeEach(async () => {
config = {
get: sinon.stub()
};
successStub = sinon.stub();
fetch = {
- default: successStub.callsFake(function(...args) {
+ default: successStub.callsFake(function (...args) {
let requestedUrl = url.parse(args[0], true);
expect(requestedUrl.query.code).to.equal(AUTH_CODE);
expect(requestedUrl.query.redirect_uri).to.equal(REDIRECT_URL);
return Promise.resolve(SUCCESSFUL_RESPONSE);
})
};
- accessTokenRequest = proxyquire('../../app/oauth2/access-token-request', {
+ accessTokenRequest = await esmock('../../app/oauth2/access-token-request.js', {
'config': config,
'node-fetch': fetch
});
unsuccessfulStub = sinon.stub();
unsuccessfulFetch = {
- default: unsuccessfulStub.callsFake(function(...args) {
+ default: unsuccessfulStub.callsFake(function (...args) {
let requestedUrl = url.parse(args[0], true);
expect(requestedUrl.query.code).to.equal(AUTH_CODE);
expect(requestedUrl.query.redirect_uri).to.equal(REDIRECT_URL);
return Promise.resolve(UNSUCCESSFUL_RESPONSE);
})
};
- unsuccessfulAccessTokenRequest = proxyquire('../../app/oauth2/access-token-request', {
+ unsuccessfulAccessTokenRequest = await esmock('../../app/oauth2/access-token-request.js', {
'config': config,
'node-fetch': unsuccessfulFetch
});
diff --git a/test/oauth2/logout-route.spec.js b/test/oauth2/logout-route.spec.js
index bab75c617..ef40ce2fb 100644
--- a/test/oauth2/logout-route.spec.js
+++ b/test/oauth2/logout-route.spec.js
@@ -1,14 +1,17 @@
-const chai = require('chai');
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
+import * as chai from 'chai';
+import {expect} from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
const assert = sinon.assert;
-const expect = chai.expect;
-const sinonChai = require('sinon-chai').default;
-const sinonExpressMock = require('sinon-express-mock');
-const ACCESS_TOKEN_COOKIE_NAME = require('../../app/oauth2/oauth2-route').COOKIE_ACCESS_TOKEN;
+import sinonChai from 'sinon-chai';
+import sinonExpressMock from 'sinon-express-mock';
+import {COOKIE_ACCESS_TOKEN} from '../../app/oauth2/oauth2-route.js';
chai.use(sinonChai);
-const nock = require('nock');
-const CacheService = require('../../app/cache/cache-service');
+import nock from 'nock';
+import CacheService from '../../app/cache/cache-service.js';
+
+let logoutRoute;
+let getCachedUserDetails;
describe('logoutRoute', () => {
const CLIENT_ID = 'ccd_gateway';
@@ -22,27 +25,25 @@ describe('logoutRoute', () => {
let response;
let next;
let config;
- let logoutRoute;
let fetchStub;
let fetch;
let userInfoCacheSpy;
let sandbox;
let clock;
+ let userInfoCacheInstance;
- let cachedUserResolver;
- let userInfoCache;
-
- beforeEach(() => {
+ beforeEach(async () => {
config = {
get: sinon.stub()
};
sandbox = sinon.createSandbox();
clock = sandbox.useFakeTimers();
- userInfoCache = new CacheService('UserInfoCache', CACHE_TTL_SECONDS, 120);
- userInfoCacheSpy = sandbox.spy(userInfoCache, 'getOrElseUpdate');
- cachedUserResolver = proxyquire('../../app/user/cached-user-resolver', {
- '../cache/cache-config': { userInfoCache }
- });
+ userInfoCacheInstance = new CacheService('UserInfoCache', CACHE_TTL_SECONDS, 120);
+ userInfoCacheSpy = sandbox.spy(userInfoCacheInstance, 'getOrElseUpdate');
+
+ ({ getCachedUserDetails } = await esmock('../../app/user/cached-user-resolver.js', {
+ '../../app/cache/cache-config.js': { userInfoCache: () => userInfoCacheInstance }
+ }));
config.get.withArgs('idam.oauth2.client_id').returns(CLIENT_ID);
config.get.withArgs('secrets.ccd.ccd-api-gateway-oauth2-client-secret').returns(CLIENT_SECRET);
@@ -50,7 +51,7 @@ describe('logoutRoute', () => {
request = sinonExpressMock.mockReq({
cookies: {
- [ACCESS_TOKEN_COOKIE_NAME]: ACCESS_TOKEN
+ [COOKIE_ACCESS_TOKEN]: ACCESS_TOKEN
}
});
response = sinonExpressMock.mockRes();
@@ -58,15 +59,16 @@ describe('logoutRoute', () => {
fetchStub = sinon.stub();
fetch = {
- default: fetchStub.callsFake(function() {
+ default: fetchStub.callsFake(function () {
return Promise.resolve({});
})
};
- logoutRoute = proxyquire('../../app/oauth2/logout-route', {
+ const result = await esmock('../../app/oauth2/logout-route.js', {
'config': config,
'node-fetch': fetch
- }).logoutRoute;
+ });
+ logoutRoute = result.logoutRoute;
});
afterEach(() => {
@@ -76,18 +78,18 @@ describe('logoutRoute', () => {
assert.fail('Not all nock interceptors have completed');
}
});
-
+
it('should call IDAM OAuth 2 logout endpoint with JWT token', done => {
response.status.callsFake(() => {
try {
assert.calledWith(fetchStub, LOGOUT_END_POINT.replace(':token', ACCESS_TOKEN));
assert.notCalled(next);
- assert.calledWith(response.clearCookie, ACCESS_TOKEN_COOKIE_NAME);
+ assert.calledWith(response.clearCookie, COOKIE_ACCESS_TOKEN);
- cachedUserResolver.getUserDetails(TOKEN);
clock.tick(CACHE_TTL_SECONDS * 1000 + 1);
- const result = cachedUserResolver.getUserDetails(TOKEN);
+ getCachedUserDetails(TOKEN);
+ const result = getCachedUserDetails(TOKEN);
expect(JSON.stringify(result)).to.equal('{}');
assert.calledWith(userInfoCacheSpy, TOKEN, sinon.match.func);
assert.calledTwice(userInfoCacheSpy);
diff --git a/test/oauth2/oauth2-route.spec.js b/test/oauth2/oauth2-route.spec.js
index 9830bd813..c5a7e23f1 100644
--- a/test/oauth2/oauth2-route.spec.js
+++ b/test/oauth2/oauth2-route.spec.js
@@ -1,10 +1,10 @@
-const chai = require('chai');
-const proxyquire = require('proxyquire');
-const expect = chai.expect;
-const sinon = require('sinon');
-const sinonChai = require('sinon-chai').default;
-const sinonExpressMock = require('sinon-express-mock');
-const ACCESS_TOKEN_COOKIE_NAME = require('../../app/oauth2/oauth2-route').COOKIE_ACCESS_TOKEN;
+import * as chai from 'chai';
+import {expect} from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
+import sinonExpressMock from 'sinon-express-mock';
+import {COOKIE_ACCESS_TOKEN} from '../../app/oauth2/oauth2-route.js';
const assert = sinon.assert;
chai.use(sinonChai);
@@ -22,7 +22,7 @@ describe('oauth2Route', () => {
let oauth2Route;
let responseFromPromiseMock;
- beforeEach(() => {
+ beforeEach(async () => {
config = {
get: sinon.stub()
@@ -38,10 +38,11 @@ describe('oauth2Route', () => {
accessTokenRequest = sinon.stub();
accessTokenRequest.withArgs(request).returns(Promise.resolve(responseFromPromiseMock));
- oauth2Route = proxyquire('../../app/oauth2/oauth2-route', {
- './access-token-request': accessTokenRequest,
- 'config': config
- }).oauth2Route;
+ const oauth2Module = await esmock('../../app/oauth2/oauth2-route.js', {
+ '../../app/oauth2/access-token-request.js': { default: accessTokenRequest },
+ config: { default: config }
+ });
+ oauth2Route = oauth2Module.oauth2Route;
});
it('should set an accessToken cookie with the "secure" flag enabled', done => {
@@ -54,7 +55,7 @@ describe('oauth2Route', () => {
assert.calledWith(accessTokenRequest, request);
assert.calledWith(config.get, 'security.secure_auth_cookie_enabled');
- assert.calledWith(response.cookie, ACCESS_TOKEN_COOKIE_NAME, TOKEN.access_token,
+ assert.calledWith(response.cookie, COOKIE_ACCESS_TOKEN, TOKEN.access_token,
{ maxAge: TOKEN.expires_in * 1000, httpOnly: true, secure: true });
assert.calledWith(response.status, 204);
done();
@@ -75,8 +76,8 @@ describe('oauth2Route', () => {
try {
assert.calledWith(accessTokenRequest, request);
assert.calledWith(config.get, 'security.secure_auth_cookie_enabled');
- assert.calledWith(response.cookie, ACCESS_TOKEN_COOKIE_NAME, TOKEN.access_token,
- { maxAge: TOKEN.expires_in * 1000, httpOnly: true, secure: false });
+ assert.calledWith(response.cookie, COOKIE_ACCESS_TOKEN, TOKEN.access_token,
+ {maxAge: TOKEN.expires_in * 1000, httpOnly: true, secure: false});
assert.calledWith(response.status, 204);
done();
} catch (e) {
@@ -88,31 +89,26 @@ describe('oauth2Route', () => {
});
it('should fail to obation an accessToken dude to unauthorized request.', done => {
+ const expectedError = { status: 502, message: 'Internal Server Error' };
- let expectedError = {
- status: 502,
- message: 'Internal Server Error'
- };
-
- let unauthorizedAccessTokenRequest = sinon.stub();
+ const unauthorizedAccessTokenRequest = sinon.stub();
unauthorizedAccessTokenRequest.withArgs(request).returns(Promise.resolve(expectedError));
- let unauthorizedOauth2Route = proxyquire('../../app/oauth2/oauth2-route', {
- './access-token-request': unauthorizedAccessTokenRequest,
- 'config': config
- }).oauth2Route;
-
- next.callsFake((result) => {
- try {
-
- assert.calledWith(unauthorizedAccessTokenRequest, request);
- expect(result).to.deep.equal(expectedError);
- done();
- } catch (e) {
- done(e);
- }
- });
-
- unauthorizedOauth2Route(request, response, next);
+ esmock('../../app/oauth2/oauth2-route.js', {
+ '../../app/oauth2/access-token-request.js': { default: unauthorizedAccessTokenRequest },
+ config: { default: config }
+ }).then(({ oauth2Route: unauthorizedOauth2Route }) => {
+ next.callsFake((result) => {
+ try {
+ assert.calledWith(unauthorizedAccessTokenRequest, request);
+ expect(result).to.deep.equal(expectedError);
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+
+ unauthorizedOauth2Route(request, response, next);
+ }).catch(done);
});
});
diff --git a/test/security/cors.spec.js b/test/security/cors.spec.js
index 6537e5a20..cbe91865d 100644
--- a/test/security/cors.spec.js
+++ b/test/security/cors.spec.js
@@ -1,9 +1,10 @@
-const chai = require('chai');
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
+import * as chai from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+
const assert = sinon.assert;
-const sinonChai = require('sinon-chai').default;
-const sinonExpressMock = require('sinon-express-mock');
+import sinonChai from 'sinon-chai';
+import sinonExpressMock from 'sinon-express-mock';
chai.use(sinonChai);
describe('CORS', () => {
@@ -15,7 +16,7 @@ describe('CORS', () => {
let config, req, res, next, handleCors;
- beforeEach(() => {
+ beforeEach(async () => {
config = {
get: sinon.stub()
};
@@ -30,7 +31,7 @@ describe('CORS', () => {
res = sinonExpressMock.mockRes({});
next = sinon.stub();
- handleCors = proxyquire('../../app/security/cors', {
+ handleCors = await esmock('../../app/security/cors.js', {
'config': config
});
});
diff --git a/test/security/hsts.spec.js b/test/security/hsts.spec.js
index 95897774f..fd9927c9f 100644
--- a/test/security/hsts.spec.js
+++ b/test/security/hsts.spec.js
@@ -1,9 +1,9 @@
-const chai = require('chai');
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
+import * as chai from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
const assert = sinon.assert;
-const sinonChai = require('sinon-chai').default;
-const sinonExpressMock = require('sinon-express-mock');
+import sinonChai from 'sinon-chai';
+import sinonExpressMock from 'sinon-express-mock';
chai.use(sinonChai);
describe('HSTS', () => {
@@ -12,12 +12,12 @@ describe('HSTS', () => {
let req, res, next, handleHSTS;
- beforeEach(() => {
+ beforeEach(async () => {
req = sinonExpressMock.mockReq();
res = sinonExpressMock.mockRes({});
next = sinon.stub();
- handleHSTS = proxyquire('../../app/security/hsts', {});
+ handleHSTS = await esmock('../../app/security/hsts.js', {});
});
it('should add STS header to response for data request', () => {
diff --git a/test/service/service-filter.spec.js b/test/service/service-filter.spec.js
index dd6751eb7..e4e05ca0e 100644
--- a/test/service/service-filter.spec.js
+++ b/test/service/service-filter.spec.js
@@ -1,13 +1,12 @@
-const proxyquire = require('proxyquire');
-const chai = require('chai');
-const expect = chai.expect;
+import { expect } from 'chai';
+import esmock from 'esmock';
describe('service filter', () => {
const request = {};
const reply = {};
- it('should return a 500 error in case of FetchError', done => {
+ it('should return a 500 error in case of FetchError', async () => {
const EnotFoundError = () => {
return Promise.reject({
name: 'FetchError',
@@ -15,51 +14,69 @@ describe('service filter', () => {
});
};
- const serviceFilter = proxyquire('../../app/service/service-filter',
+ const serviceFilter = await esmock('../../app/service/service-filter.js',
{
- './service-token-generator': EnotFoundError
+ '../../app/service/service-token-generator.js': {default : EnotFoundError}
});
+ await new Promise((resolve, reject) => {
serviceFilter(request, reply, (error) => {
- expect(error.status).to.equal(500);
- expect(error.error).to.equal('Internal Server Error');
- expect(error.message).to.equal('some error');
- done();
+ try {
+ expect(error.status).to.equal(500);
+ expect(error.error).to.equal('Internal Server Error');
+ expect(error.message).to.equal('some error');
+ resolve();
+ } catch (assertionError) {
+ reject(assertionError);
+ }
});
+ });
});
- it('should return a 401 error when s2s call fails with no error status', done => {
+ it('should return a 401 error when s2s call fails with no error status', async () => {
const ErrorWithNoStatus = () => {
return Promise.reject({});
};
- const serviceFilter = proxyquire('../../app/service/service-filter',
+ const serviceFilter = await esmock('../../app/service/service-filter.js',
{
- './service-token-generator': ErrorWithNoStatus
+ '../../app/service/service-token-generator.js': ErrorWithNoStatus
});
+ await new Promise((resolve, reject) => {
serviceFilter(request, reply, (error) => {
- expect(error.status).to.equal(401);
- done();
+ try {
+ expect(error.status).to.equal(401);
+ resolve();
+ } catch (assertionError) {
+ reject(assertionError);
+ }
});
+ });
});
- it('should return the error status when s2s call fails with an error status', done => {
+ it('should return the error status when s2s call fails with an error status', async () => {
const ErrorWithStatus = () => {
return Promise.reject({
status: 502
});
};
- const serviceFilter = proxyquire('../../app/service/service-filter',
+ const serviceFilter = await esmock('../../app/service/service-filter.js',
{
- './service-token-generator': ErrorWithStatus
+ '../../app/service/service-token-generator.js': ErrorWithStatus
});
+ await new Promise((resolve, reject) => {
serviceFilter(request, reply, (error) => {
- expect(error.status).to.equal(502);
- done();
+ try {
+ expect(error.status).to.equal(502);
+ resolve();
+ } catch (assertionError) {
+ reject(assertionError);
+ }
});
+ });
});
diff --git a/test/service/service-payloadGuard.spec.js b/test/service/service-payloadGuard.spec.js
new file mode 100644
index 000000000..c04b1fa11
--- /dev/null
+++ b/test/service/service-payloadGuard.spec.js
@@ -0,0 +1,208 @@
+import { expect } from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+
+// Use CommonJS import for the ES module
+describe('service-payloadGuard middleware', () => {
+ let payloadGuard, LoggerStub, loggerStub;
+
+ beforeEach(async () => {
+ loggerStub = {warn: sinon.spy(), info: sinon.spy()};
+ LoggerStub = {getLogger: sinon.stub().returns(loggerStub)};
+ payloadGuard = await esmock('../../app/service/service-payloadGuard.js', {
+ '@hmcts/nodejs-logging': {Logger: LoggerStub}
+ });
+ });
+
+ function mockReqRes(method, opts = {}) {
+ const req = {
+ method,
+ headers: opts.headers || {},
+ url: opts.url || '/test',
+ body: opts.body
+ };
+ const res = {
+ status: sinon.stub().returnsThis(),
+ json: sinon.stub().returnsThis()
+ };
+ const next = sinon.spy();
+ return { req, res, next };
+ }
+
+ it('should call next for allowed content-type', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { foo: 'bar' }
+ });
+ payloadGuard()(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+ it('should reject unsupported content-type on POST', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'text/plain' },
+ body: { foo: 'bar' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(415)).to.be.true;
+ expect(res.json.calledWithMatch({ error: 'Unsupported Media Type' })).to.be.true;
+ expect(next.notCalled).to.be.true;
+ });
+
+ it('should reject unsupported content-type on PUT', () => {
+ const { req, res, next } = mockReqRes('PUT', {
+ headers: { 'content-type': 'text/plain' },
+ body: { name: 'name' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(415)).to.be.true;
+ });
+
+
+ it('should reject unsupported content-type on PATCH', () => {
+ const { req, res, next } = mockReqRes('PATCH', {
+ headers: { 'content-type': 'text/plain' },
+ body: { age: 'age' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(415)).to.be.true;
+ });
+
+ it('should reject path traversal attempts', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ url: '/../etc/passwd',
+ headers: { 'content-type': 'application/json', host: 'localhost' },
+ body: { foo: 'bar' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(400)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Invalid path' })).to.be.true;
+ expect(next.notCalled).to.be.true;
+ });
+
+ it('should reject payloads that are too large or deeply nested', () => {
+ // Create a deeply nested object
+ let obj = {};
+ let cur = obj;
+ for (let i = 0; i < 50; i++) {
+ cur.nest = {};
+ cur = cur.nest;
+ }
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: obj
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(413)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Payload too large or too deeply nested' })).to.be.true;
+ expect(next.notCalled).to.be.true;
+ });
+
+ it('should reject payloads with arrays exceeding maxArrayLength', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { items: new Array(10001).fill('x') }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(413)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Payload too large or too deeply nested' })).to.be.true;
+ expect(next.notCalled).to.be.true;
+ });
+
+ it('should reject payloads containing ' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(400)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Payload contains disallowed content' })).to.be.true;
+ expect(next.notCalled).to.be.true;
+ });
+
+ it('should reject payloads containing javascript', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { url: 'javascript:alert(1)' }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(400)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Payload contains disallowed content' })).to.be.true;
+ });
+
+ it('should reject script tags in nested body objects', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { a: { b: { c: '' } } }
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(400)).to.be.true;
+ });
+
+ it('should call next for non-POST/PUT/PATCH methods', () => {
+ const { req, res, next } = mockReqRes('GET', {
+ headers: { 'content-type': 'application/json' },
+ body: { foo: 'bar' }
+ });
+ payloadGuard()(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+ it('should call next for application/json with charset', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json; charset=utf-8' },
+ body: { foo: 'bar' }
+ });
+ payloadGuard()(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+ it('should respect custom maxArrayLength option', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { items: new Array(5).fill('x') }
+ });
+ payloadGuard({ maxArrayLength: 3 })(req, res, next);
+ expect(res.status.calledWith(413)).to.be.true;
+ });
+
+ it('should reject percent-encoded path traversal', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ url: '/%2e%2e/etc/passwd',
+ headers: { 'content-type': 'application/json', host: 'localhost' },
+ body: {}
+ });
+ payloadGuard()(req, res, next);
+ expect(res.status.calledWith(400)).to.be.true;
+ expect(res.json.calledWithMatch({ message: 'Invalid path' })).to.be.true;
+ });
+
+ it('should not reject path traversal when rejectPathTraversal is false', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ url: '/../etc/passwd',
+ headers: { 'content-type': 'application/json', host: 'localhost' },
+ body: {}
+ });
+ payloadGuard({ rejectPathTraversal: false })(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+ it('should not reject script tags when rejectObviousScriptTags is false', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'application/json' },
+ body: { foo: '' }
+ });
+ payloadGuard({ rejectObviousScriptTags: false })(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+ it('should skip content-type check when allowContentTypes is empty', () => {
+ const { req, res, next } = mockReqRes('POST', {
+ headers: { 'content-type': 'text/xml' },
+ body: {}
+ });
+ payloadGuard({ allowContentTypes: [] })(req, res, next);
+ expect(next.calledOnce).to.be.true;
+ });
+
+});
diff --git a/test/service/service-token-generator.spec.js b/test/service/service-token-generator.spec.js
index 6f5f3e005..9191c0753 100644
--- a/test/service/service-token-generator.spec.js
+++ b/test/service/service-token-generator.spec.js
@@ -1,16 +1,15 @@
-const chai = require('chai');
-const expect = chai.expect;
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
-const nock = require('nock');
-const jwt = require('jsonwebtoken');
-const moment = require('moment');
+import { expect } from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+import nock from 'nock';
+import jwt from 'jsonwebtoken';
+import moment from 'moment';
describe('service token generator', () => {
let serviceTokenGenerator;
- beforeEach(() => {
+ beforeEach(async () => {
let config = {
get: sinon.stub()
};
@@ -18,11 +17,11 @@ describe('service token generator', () => {
config.get.withArgs('idam.s2s_url').returns('http://localhost:9999');
config.get.withArgs('appInsights.enabled').returns(false);
- serviceTokenGenerator = proxyquire('../../app/service/service-token-generator', {
+ serviceTokenGenerator = await esmock('../../app/service/service-token-generator', {
'config': config
});
- proxyquire('../../app/app-insights/app-insights', {
+ await esmock('../../app/app-insights/app-insights', {
'config': config
});
});
diff --git a/test/user/auth-checker-user-only-filter.spec.js b/test/user/auth-checker-user-only-filter.spec.js
index 76d9427c5..1c0ffeaa2 100644
--- a/test/user/auth-checker-user-only-filter.spec.js
+++ b/test/user/auth-checker-user-only-filter.spec.js
@@ -1,8 +1,8 @@
-const chai = require('chai');
-const expect = chai.expect;
-const proxyquire = require('proxyquire');
-const sinon = require('sinon');
-const sinonChai = require('sinon-chai').default;
+import * as chai from 'chai';
+import { expect } from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
chai.use(sinonChai);
describe('authCheckerUserOnlyFilter', () => {
@@ -17,7 +17,7 @@ describe('authCheckerUserOnlyFilter', () => {
let userRequestAuthorizer;
let filter;
- beforeEach(() => {
+ beforeEach(async () => {
req = {
headers: {}
};
@@ -34,8 +34,8 @@ describe('authCheckerUserOnlyFilter', () => {
authorise: sinon.stub()
};
- filter = proxyquire('../../app/user/auth-checker-user-only-filter', {
- './user-request-authorizer': userRequestAuthorizer
+ filter = await esmock('../../app/user/auth-checker-user-only-filter.js', {
+ '../../app/user/user-request-authorizer.js': userRequestAuthorizer
});
});
diff --git a/test/user/authorised-roles-extractor.spec.js b/test/user/authorised-roles-extractor.spec.js
index f2916f013..59a83d0bc 100644
--- a/test/user/authorised-roles-extractor.spec.js
+++ b/test/user/authorised-roles-extractor.spec.js
@@ -1,8 +1,7 @@
-const chai = require('chai');
-const expect = chai.expect;
+import { expect } from 'chai';
-const authorisedRolesExtractor = require('../../app/user/authorised-roles-extractor');
-const http = require('http');
+import authorisedRolesExtractor from '../../app/user/authorised-roles-extractor.js';
+import http from 'http';
describe('Authorised roles extractor', () => {
describe('extract()', () => {
@@ -10,7 +9,7 @@ describe('Authorised roles extractor', () => {
let request = Object.create(http.IncomingMessage.prototype);
request.originalUrl = '/data/';
- let roles = authorisedRolesExtractor.extract(request);
+ let roles = authorisedRolesExtractor(request);
expect(roles.length).to.equal(0);
});
@@ -19,7 +18,7 @@ describe('Authorised roles extractor', () => {
let request = Object.create(http.IncomingMessage.prototype);
request.originalUrl = '/data/caseworkers/5/profile';
- let roles = authorisedRolesExtractor.extract(request);
+ let roles = authorisedRolesExtractor(request);
expect(roles).to.contain('caseworker');
expect(roles.length).to.equal(1);
@@ -29,7 +28,7 @@ describe('Authorised roles extractor', () => {
let request = Object.create(http.IncomingMessage.prototype);
request.originalUrl = '/data/CaseWorkers/5/jurisdiction/TEST/case-types/TestAddressBookCase/cases/7';
- let roles = authorisedRolesExtractor.extract(request);
+ let roles = authorisedRolesExtractor(request);
expect(roles).to.contain('caseworker-test');
expect(roles.length).to.equal(1);
diff --git a/test/user/cached-user-resolver.spec.js b/test/user/cached-user-resolver.spec.js
index c500e8f99..a147b1cb0 100644
--- a/test/user/cached-user-resolver.spec.js
+++ b/test/user/cached-user-resolver.spec.js
@@ -1,13 +1,13 @@
-const proxyquire = require('proxyquire');
-const chai = require('chai');
-const expect = chai.expect;
-const sinon = require('sinon');
+import * as chai from 'chai';
+import { expect } from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
const assert = sinon.assert;
-const sinonChai = require('sinon-chai').default;
+import sinonChai from 'sinon-chai';
chai.use(sinonChai);
-const nock = require('nock');
-const CacheService = require('../../app/cache/cache-service');
-const NodeCache = require('node-cache');
+import nock from 'nock';
+import CacheService from '../../app/cache/cache-service.js';
+import NodeCache from 'node-cache';
describe('getCachedUserDetails', () => {
const TOKEN = 'TOKEN';
@@ -22,9 +22,9 @@ describe('getCachedUserDetails', () => {
let clock;
let userResolver;
let cachedUserResolver;
- let userInfoCache;
+ let userInfoCacheInstance;
- beforeEach(() => {
+ beforeEach(async () => {
sandbox = sinon.createSandbox();
userResolver = {
getUserDetails: sinon.stub()
@@ -35,10 +35,10 @@ describe('getCachedUserDetails', () => {
userResolver.getUserDetails.withArgs(ERROR_TOKEN).returns(Promise.reject(new Error('User details error')));
clock = sandbox.useFakeTimers();
nodeCacheSpy = sandbox.spy(NodeCache.prototype, 'set');
- userInfoCache = new CacheService('UserInfoCache', CACHE_TTL_SECONDS, 120);
- cachedUserResolver = proxyquire('../../app/user/cached-user-resolver', {
- '../cache/cache-config': { userInfoCache },
- './user-resolver': userResolver
+ userInfoCacheInstance = new CacheService('UserInfoCache', CACHE_TTL_SECONDS, 120);
+ cachedUserResolver = await esmock('../../app/user/cached-user-resolver.js', {
+ '../../app/cache/cache-config.js': { userInfoCache: () => userInfoCacheInstance },
+ '../../app/user/user-resolver.js': { getUserDetails: userResolver.getUserDetails }
});
});
@@ -51,15 +51,15 @@ describe('getCachedUserDetails', () => {
});
it('should get user details with no cache', async () => {
- const result = await cachedUserResolver.getUserDetails(TOKEN);
+ const result = await cachedUserResolver.getCachedUserDetails(TOKEN);
expect(JSON.stringify(result)).to.equal(JSON.stringify(USER_DETAILS));
assert.calledOnce(nodeCacheSpy);
});
it('should get cached user details', async () => {
- const firstResult = await cachedUserResolver.getUserDetails(TOKEN);
- const cachedResult = await cachedUserResolver.getUserDetails(TOKEN);
+ const firstResult = await cachedUserResolver.getCachedUserDetails(TOKEN);
+ const cachedResult = await cachedUserResolver.getCachedUserDetails(TOKEN);
expect(JSON.stringify(firstResult)).to.equal(JSON.stringify(cachedResult));
expect(JSON.stringify(cachedResult)).to.equal(JSON.stringify(USER_DETAILS));
@@ -67,8 +67,8 @@ describe('getCachedUserDetails', () => {
});
it('should get cached user details with token including Bearer', async () => {
- const firstResult = await cachedUserResolver.getUserDetails(TOKEN);
- const cachedResult = await cachedUserResolver.getUserDetails(`Bearer ${TOKEN}`);
+ const firstResult = await cachedUserResolver.getCachedUserDetails(TOKEN);
+ const cachedResult = await cachedUserResolver.getCachedUserDetails(`Bearer ${TOKEN}`);
expect(JSON.stringify(firstResult)).to.equal(JSON.stringify(cachedResult));
expect(JSON.stringify(cachedResult)).to.equal(JSON.stringify(USER_DETAILS));
@@ -76,8 +76,8 @@ describe('getCachedUserDetails', () => {
});
it('should get user details with no cache for different tokens', async () => {
- const originalResult = await cachedUserResolver.getUserDetails(TOKEN);
- const otherResult = await cachedUserResolver.getUserDetails(OTHER_TOKEN);
+ const originalResult = await cachedUserResolver.getCachedUserDetails(TOKEN);
+ const otherResult = await cachedUserResolver.getCachedUserDetails(OTHER_TOKEN);
expect(JSON.stringify(originalResult)).to.equal(JSON.stringify(USER_DETAILS));
expect(JSON.stringify(otherResult)).to.equal(JSON.stringify(OTHER_DETAILS));
@@ -85,10 +85,10 @@ describe('getCachedUserDetails', () => {
});
it('should get new user details after ttl expiry', async () => {
- await cachedUserResolver.getUserDetails(TOKEN);
+ await cachedUserResolver.getCachedUserDetails(TOKEN);
clock.tick(CACHE_TTL_SECONDS * 1000 + 1);
- const result = await cachedUserResolver.getUserDetails(TOKEN);
+ const result = await cachedUserResolver.getCachedUserDetails(TOKEN);
expect(JSON.stringify(result)).to.equal(JSON.stringify(USER_DETAILS));
assert.calledTwice(nodeCacheSpy);
@@ -98,7 +98,7 @@ describe('getCachedUserDetails', () => {
let result;
let error;
try {
- result = await cachedUserResolver.getUserDetails(ERROR_TOKEN);
+ result = await cachedUserResolver.getCachedUserDetails(ERROR_TOKEN);
} catch(err) {
error = err;
}
diff --git a/test/user/user-request-authorizer.spec.js b/test/user/user-request-authorizer.spec.js
index c8568abeb..8f8045272 100644
--- a/test/user/user-request-authorizer.spec.js
+++ b/test/user/user-request-authorizer.spec.js
@@ -1,11 +1,12 @@
-const proxyquire = require('proxyquire');
-const chai = require('chai');
-const sinon = require('sinon');
-const expect = chai.expect;
-const sinonChai = require('sinon-chai').default;
+import * as chai from 'chai';
+import {expect} from 'chai';
+import esmock from 'esmock';
+import sinon from 'sinon';
+import sinonChai from 'sinon-chai';
+
chai.use(sinonChai);
-const userReqAuth = require('../../app/user/user-request-authorizer');
-const COOKIE_ACCESS_TOKEN = require('../../app/oauth2/oauth2-route').COOKIE_ACCESS_TOKEN;
+import {AUTHORIZATION} from '../../app/user/user-request-authorizer.js';
+import {COOKIE_ACCESS_TOKEN} from '../../app/oauth2/oauth2-route.js';
describe('UserRequestAuthorizer', () => {
describe('authorize', () => {
@@ -25,10 +26,9 @@ describe('UserRequestAuthorizer', () => {
let request;
let userResolver;
let authorizedRolesExtractor;
-
let userRequestAuthorizer;
- beforeEach(() => {
+ beforeEach(async () => {
request = {
url: 'http://caseworkers/:uid/more/stuff',
originalUrl: 'http://caseworkers/:uid/more/stuff',
@@ -36,18 +36,18 @@ describe('UserRequestAuthorizer', () => {
cookies: COOKIES
};
userResolver = {
- getUserDetails: sinon.stub().returns(Promise.resolve(DETAILS))
+ getUserDetails: sinon.stub().resolves(DETAILS),
+ getCachedUserDetails: sinon.stub().resolves(DETAILS)
};
+
authorizedRolesExtractor = {
- extract: sinon.stub()
+ default: sinon.stub()
};
- delete require.cache[require.resolve('../../app/user/user-request-authorizer')];
-
- userRequestAuthorizer = proxyquire('../../app/user/user-request-authorizer', {
- './cached-user-resolver': userResolver,
- './user-resolver': userResolver,
- './authorised-roles-extractor': authorizedRolesExtractor
+ userRequestAuthorizer = await esmock('../../app/user/user-request-authorizer.js', {
+ '../../app/user/cached-user-resolver.js': userResolver,
+ '../../app/user/user-resolver.js': userResolver,
+ '../../app/user/authorised-roles-extractor.js': authorizedRolesExtractor
});
});
@@ -76,7 +76,7 @@ describe('UserRequestAuthorizer', () => {
});
it('should reject when roles do not match', done => {
- authorizedRolesExtractor.extract.returns(['no-match']);
+ authorizedRolesExtractor.default.returns(['no-match']);
userRequestAuthorizer.authorise(request)
.then(() => done(new Error('Promise should have been rejected')))
@@ -87,7 +87,7 @@ describe('UserRequestAuthorizer', () => {
});
it('should reject when no roles are extracted', done => {
- authorizedRolesExtractor.extract.returns([]);
+ authorizedRolesExtractor.default.returns([]);
userRequestAuthorizer.authorise(request)
.then(() => done(new Error('Promise should have been rejected')))
@@ -98,7 +98,7 @@ describe('UserRequestAuthorizer', () => {
});
it('should fill in user ID placeholder in URL', done => {
- authorizedRolesExtractor.extract.returns([ROLE_1]);
+ authorizedRolesExtractor.default.returns([ROLE_1]);
userRequestAuthorizer.authorise(request)
.then(() => {
@@ -111,7 +111,7 @@ describe('UserRequestAuthorizer', () => {
it('should resolve with user details when all checks OK', done => {
request.cookies = null;
- authorizedRolesExtractor.extract.returns([ROLE_1]);
+ authorizedRolesExtractor.default.returns([ROLE_1]);
userRequestAuthorizer.authorise(request)
.then(user => {
@@ -123,7 +123,7 @@ describe('UserRequestAuthorizer', () => {
it('should NOT reject missing Authorization header when AccessToken cookie present', done => {
request.get.returns(null);
- authorizedRolesExtractor.extract.returns([ROLE_1]);
+ authorizedRolesExtractor.default.returns([ROLE_1]);
userRequestAuthorizer.authorise(request)
.then(() => done())
@@ -135,7 +135,7 @@ describe('UserRequestAuthorizer', () => {
it('should use the AccessToken cookie when present, to obtain user details', done => {
request.get.returns(null);
- authorizedRolesExtractor.extract.returns([ROLE_1]);
+ authorizedRolesExtractor.default.returns([ROLE_1]);
userRequestAuthorizer.authorise(request)
.then(() => {
@@ -145,19 +145,16 @@ describe('UserRequestAuthorizer', () => {
.catch(() => done(new Error('Promise should have been resolved')));
});
- it('should use the AccessToken cookie to set the Authorization header, when the header is missing', done => {
+ it('should use the AccessToken cookie to set the Authorization header, when the header is missing', async () => {
request.get.returns(null);
request.headers = {'X_CUSTOM_HEADER': X_CUSTOM_HEADER};
- authorizedRolesExtractor.extract.returns([ROLE_1]);
+ authorizedRolesExtractor.default.returns([ROLE_1]);
- userRequestAuthorizer.authorise(request)
- .then(() => {
- expect(request.headers).not.to.be.undefined;
- expect(request.headers[userReqAuth.AUTHORIZATION]).to.equal('Bearer ' + COOKIES[COOKIE_ACCESS_TOKEN]);
- expect(request.headers['X_CUSTOM_HEADER']).to.equal(X_CUSTOM_HEADER);
- done();
- })
- .catch(() => done(new Error('Promise should have been resolved')));
+ await userRequestAuthorizer.authorise(request);
+
+ expect(request.headers).not.to.be.undefined;
+ expect(request.headers[AUTHORIZATION]).to.equal('Bearer ' + COOKIES[COOKIE_ACCESS_TOKEN]);
+ expect(request.headers['X_CUSTOM_HEADER']).to.equal(X_CUSTOM_HEADER);
});
it('should NOT check roles and resolve when URL does not contain /caseworkers/', done => {
@@ -166,7 +163,7 @@ describe('UserRequestAuthorizer', () => {
userRequestAuthorizer.authorise(request)
.then(user => {
- expect(authorizedRolesExtractor.extract).not.to.have.been.called;
+ expect(authorizedRolesExtractor.default).not.to.have.been.called;
expect(user).to.equal(DETAILS);
done();
})
@@ -179,7 +176,7 @@ describe('UserRequestAuthorizer', () => {
userRequestAuthorizer.authorise(request)
.then(user => {
- expect(authorizedRolesExtractor.extract).not.to.have.been.called;
+ expect(authorizedRolesExtractor.default).not.to.have.been.called;
expect(user).to.equal(DETAILS);
done();
})
@@ -187,7 +184,7 @@ describe('UserRequestAuthorizer', () => {
});
it('should resolve when user has at least one matching role', done => {
- authorizedRolesExtractor.extract.returns([ROLE_1, 'other-role']);
+ authorizedRolesExtractor.default.returns([ROLE_1, 'other-role']);
userRequestAuthorizer.authorise(request)
.then(user => {
@@ -198,7 +195,7 @@ describe('UserRequestAuthorizer', () => {
});
describe('static role protected paths', () => {
- beforeEach(() => {
+ beforeEach(async () => {
request = {
url: '/print/probateManTypes',
originalUrl: '/print/probateManTypes',
@@ -206,12 +203,10 @@ describe('UserRequestAuthorizer', () => {
cookies: COOKIES
};
- delete require.cache[require.resolve('../../app/user/user-request-authorizer')];
-
- userRequestAuthorizer = proxyquire('../../app/user/user-request-authorizer', {
- './cached-user-resolver': userResolver,
- './user-resolver': userResolver,
- './authorised-roles-extractor': authorizedRolesExtractor
+ userRequestAuthorizer = await esmock('../../app/user/user-request-authorizer.js', {
+ '../../app/user/cached-user-resolver.js': userResolver,
+ '../../app/user/user-resolver.js': userResolver,
+ '../../app/user/authorised-roles-extractor.js': authorizedRolesExtractor
});
});
@@ -274,20 +269,17 @@ describe('UserRequestAuthorizer', () => {
});
});
- it('should reject with UNAUTHORISED_ROLE when user has no roles for /print/probateManTypes', done => {
- userResolver.getUserDetails.returns(Promise.resolve({
- uid: USER_ID,
- roles: []
- }));
+ it('should reject with UNAUTHORISED_ROLE when user has no roles for /print/probateManTypes', async () => {
+ userResolver.getUserDetails.resolves({ uid: USER_ID, roles: [] });
- userRequestAuthorizer.authorise(request)
- .then(() => done(new Error('Promise should have been rejected')))
- .catch(error => {
- expect(error).to.equal(userRequestAuthorizer.ERROR_UNAUTHORISED_ROLE);
- expect(error.status).to.equal(403);
- expect(error.error).to.equal('Unauthorised role');
- done();
- });
+ try {
+ await userRequestAuthorizer.authorise(request);
+ throw new Error('Promise should have been rejected');
+ } catch (error) {
+ expect(error).to.equal(userRequestAuthorizer.ERROR_UNAUTHORISED_ROLE);
+ expect(error.status).to.equal(403);
+ expect(error.error).to.equal('Unauthorised role');
+ }
});
it('should resolve for a path that does not match any static protected path', done => {
diff --git a/test/util/jwt.spec.js b/test/util/jwt.spec.js
index 9cc8bfaa0..840db277c 100644
--- a/test/util/jwt.spec.js
+++ b/test/util/jwt.spec.js
@@ -1,7 +1,6 @@
-const chai = require('chai');
-const expect = chai.expect;
+import {expect} from 'chai';
-const jwtUtil = require('../../app/util/jwt');
+import * as jwtUtil from '../../app/util/jwt.js';
describe('get bearer jwt', () => {
it('should return bearer where jwt has prefix', () => {
diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues
index 288061e14..13994e394 100644
--- a/yarn-audit-known-issues
+++ b/yarn-audit-known-issues
@@ -1,5 +1,4 @@
-{"value":"@opentelemetry/core","children":{"ID":1120821,"Issue":"OpenTelemetry Core: Unbounded memory allocation in W3C Baggage propagation","URL":"https://github.com/advisories/GHSA-8988-4f7v-96qf","Severity":"moderate","Vulnerable Versions":"<2.8.0","Tree Versions":["2.7.1"],"Dependents":["@opentelemetry/exporter-logs-otlp-http@virtual:f929e7efe4e527b9e7f342a5744200c3a98dadc4585c20076ed75c954b440d663510efc3b6317980affff22ca3411dac18adf9bae8f716421eb973cca60806cb#npm:0.217.0"]}}
-{"value":"js-yaml","children":{"ID":1120792,"Issue":"JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases","URL":"https://github.com/advisories/GHSA-h67p-54hq-rp68","Severity":"moderate","Vulnerable Versions":"<=4.1.1","Tree Versions":["3.14.2"],"Dependents":["@istanbuljs/load-nyc-config@npm:1.1.0"]}}
+{"value":"js-yaml","children":{"ID":1121859,"Issue":"JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases","URL":"https://github.com/advisories/GHSA-h67p-54hq-rp68","Severity":"moderate","Vulnerable Versions":"<3.15.0","Tree Versions":["3.14.2"],"Dependents":["@istanbuljs/load-nyc-config@npm:1.1.0"]}}
{"value":"mem","children":{"ID":"mem (deprecation)","Issue":"Renamed to memoize: https://www.npmjs.com/package/memoize","Severity":"moderate","Vulnerable Versions":"10.0.0","Tree Versions":["10.0.0"],"Dependents":["ccd-api-gateway-web@workspace:."]}}
{"value":"node-domexception","children":{"ID":"node-domexception (deprecation)","Issue":"Use your platform's native DOMException instead","Severity":"moderate","Vulnerable Versions":"1.0.0","Tree Versions":["1.0.0"],"Dependents":["fetch-blob@npm:3.2.0"]}}
{"value":"protobufjs","children":{"ID":1118640,"Issue":"protobuf.js: Code injection through bytes field defaults in generated toObject code","URL":"https://github.com/advisories/GHSA-66ff-xgx4-vchm","Severity":"high","Vulnerable Versions":">=8.0.0 <=8.0.1","Tree Versions":["8.0.1"],"Dependents":["@opentelemetry/otlp-transformer@virtual:1b6880599216cbe889955e567228ece139af5b29d0a893c8d13b5f3084faba5c068facdcae34bba7be1a659e74308f639e73aa9f19f4d37f7bf44a55beff324e#npm:0.217.0"]}}
diff --git a/yarn.lock b/yarn.lock
index 2749422d2..11f8146a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -476,6 +476,13 @@ __metadata:
languageName: node
linkType: hard
+"@bcoe/v8-coverage@npm:^1.0.1":
+ version: 1.0.2
+ resolution: "@bcoe/v8-coverage@npm:1.0.2"
+ checksum: 10/46600b2dde460269b07a8e4f12b72e418eae1337b85c979f43af3336c9a1c65b04e42508ab6b245f1e0e3c64328e1c38d8cd733e4a7cebc4fbf9cf65c6e59937
+ languageName: node
+ linkType: hard
+
"@colors/colors@npm:1.6.0":
version: 1.6.0
resolution: "@colors/colors@npm:1.6.0"
@@ -751,7 +758,7 @@ __metadata:
languageName: node
linkType: hard
-"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28":
+"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28":
version: 0.3.31
resolution: "@jridgewell/trace-mapping@npm:0.3.31"
dependencies:
@@ -884,18 +891,7 @@ __metadata:
languageName: node
linkType: hard
-"@opentelemetry/core@npm:2.7.1":
- version: 2.7.1
- resolution: "@opentelemetry/core@npm:2.7.1"
- dependencies:
- "@opentelemetry/semantic-conventions": "npm:^1.29.0"
- peerDependencies:
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- checksum: 10/aaec929e1f22b3704e3cb195f8a27b2d9bd0292a565741c00b1e8cde0f3043de41713632fe15e4bfd588d83807a5b10a861deffad173e28787802ede9123ced7
- languageName: node
- linkType: hard
-
-"@opentelemetry/core@npm:2.8.0, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.7.1":
+"@opentelemetry/core@npm:2.8.0":
version: 2.8.0
resolution: "@opentelemetry/core@npm:2.8.0"
dependencies:
@@ -1780,6 +1776,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/istanbul-lib-coverage@npm:^2.0.1":
+ version: 2.0.6
+ resolution: "@types/istanbul-lib-coverage@npm:2.0.6"
+ checksum: 10/3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778
+ languageName: node
+ linkType: hard
+
"@types/json-schema@npm:^7.0.15":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
@@ -2365,6 +2368,32 @@ __metadata:
languageName: node
linkType: hard
+"c8@npm:^11.0.0":
+ version: 11.0.0
+ resolution: "c8@npm:11.0.0"
+ dependencies:
+ "@bcoe/v8-coverage": "npm:^1.0.1"
+ "@istanbuljs/schema": "npm:^0.1.3"
+ find-up: "npm:^5.0.0"
+ foreground-child: "npm:^3.1.1"
+ istanbul-lib-coverage: "npm:^3.2.0"
+ istanbul-lib-report: "npm:^3.0.1"
+ istanbul-reports: "npm:^3.1.6"
+ test-exclude: "npm:^8.0.0"
+ v8-to-istanbul: "npm:^9.0.0"
+ yargs: "npm:^17.7.2"
+ yargs-parser: "npm:^21.1.1"
+ peerDependencies:
+ monocart-coverage-reports: ^2
+ peerDependenciesMeta:
+ monocart-coverage-reports:
+ optional: true
+ bin:
+ c8: bin/c8.js
+ checksum: 10/4c01de0fa49dab5b99bcb85bfff359764b7c027028f390841ece6e907c6ecb33eef8253291564665a5ddc81c39983947327bf0e12db88a8ab42fd5dffaf73b4b
+ languageName: node
+ linkType: hard
+
"caching-transform@npm:^4.0.0":
version: 4.0.0
resolution: "caching-transform@npm:4.0.0"
@@ -2437,6 +2466,7 @@ __metadata:
applicationinsights: "npm:3.15.0"
body-parser: "npm:^2.2.2"
brace-expansion: "npm:^5.0.6"
+ c8: "npm:^11.0.0"
chai: "npm:^6.2.2"
chai-http: "npm:^5.1.2"
config: "npm:^4.4.1"
@@ -2447,6 +2477,7 @@ __metadata:
dont-sniff-mimetype: "npm:^1.1.0"
eslint: "npm:^10.4.1"
eslint-plugin-mocha: "npm:^11.3.0"
+ esmock: "npm:^2.7.6"
express: "npm:^5.2.1"
fetch-mock: "npm:^12.6.0"
form-data: "npm:^4.0.5"
@@ -3275,6 +3306,13 @@ __metadata:
languageName: node
linkType: hard
+"esmock@npm:^2.7.6":
+ version: 2.7.6
+ resolution: "esmock@npm:2.7.6"
+ checksum: 10/46563a047c6c745be7553b62ddb162422a05b180fb2f98257e5b15f0c76271c3a63e9952ed63138a55236f3868187871eccebfac7eb1db0326ce2ae96c1199ab
+ languageName: node
+ linkType: hard
+
"espree@npm:^10.0.1":
version: 10.4.0
resolution: "espree@npm:10.4.0"
@@ -3631,7 +3669,7 @@ __metadata:
languageName: node
linkType: hard
-"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.0":
+"foreground-child@npm:^3.1.0, foreground-child@npm:^3.1.1, foreground-child@npm:^3.3.0":
version: 3.3.1
resolution: "foreground-child@npm:3.3.1"
dependencies:
@@ -4418,7 +4456,7 @@ __metadata:
languageName: node
linkType: hard
-"istanbul-lib-report@npm:^3.0.0":
+"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1":
version: 3.0.1
resolution: "istanbul-lib-report@npm:3.0.1"
dependencies:
@@ -4440,7 +4478,7 @@ __metadata:
languageName: node
linkType: hard
-"istanbul-reports@npm:^3.0.2":
+"istanbul-reports@npm:^3.0.2, istanbul-reports@npm:^3.1.6":
version: 3.2.0
resolution: "istanbul-reports@npm:3.2.0"
dependencies:
@@ -6875,6 +6913,17 @@ __metadata:
languageName: node
linkType: hard
+"v8-to-istanbul@npm:^9.0.0":
+ version: 9.3.0
+ resolution: "v8-to-istanbul@npm:9.3.0"
+ dependencies:
+ "@jridgewell/trace-mapping": "npm:^0.3.12"
+ "@types/istanbul-lib-coverage": "npm:^2.0.1"
+ convert-source-map: "npm:^2.0.0"
+ checksum: 10/fb1d70f1176cb9dc46cabbb3fd5c52c8f3e8738b61877b6e7266029aed0870b04140e3f9f4550ac32aebcfe1d0f38b0bac57e1e8fb97d68fec82f2b416148166
+ languageName: node
+ linkType: hard
+
"vary@npm:^1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"