From 99dddbba763e03c2909433e78560f362284561f7 Mon Sep 17 00:00:00 2001 From: Farrel Athaillah Date: Sun, 28 Sep 2025 12:24:51 +0000 Subject: [PATCH 1/3] feat(fix) for user Test Unit --- __tests__/api/routes/userRoutes.test.ts | 9 ++ fix.sh | 158 ++++++++++++++++++++++++ test/utils/testServer.ts | 29 +++++ 3 files changed, 196 insertions(+) create mode 100644 __tests__/api/routes/userRoutes.test.ts create mode 100755 fix.sh create mode 100644 test/utils/testServer.ts diff --git a/__tests__/api/routes/userRoutes.test.ts b/__tests__/api/routes/userRoutes.test.ts new file mode 100644 index 0000000..9610445 --- /dev/null +++ b/__tests__/api/routes/userRoutes.test.ts @@ -0,0 +1,9 @@ +import { http, BASE } from '../../../test/utils/testServer'; + +describe('userRoutes', () => { + it('mounts and returns 404/401/403 for an unknown subroute (no 5xx)', async () => { + const res = await http().get(`${BASE}/__unknown__`); + expect(res.status).toBeGreaterThanOrEqual(401); // could be 401/403/404 + expect(res.status).toBeLessThan(500); // must not 5xx + }); +}); diff --git a/fix.sh b/fix.sh new file mode 100755 index 0000000..4f26391 --- /dev/null +++ b/fix.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +set -euo pipefail + +banner() { printf "\n\033[1;36m%s\033[0m\n" "▸ $*"; } + +# 0) Guards +[ -f package.json ] || { echo "❌ Run this from the repo root (package.json not found)."; exit 1; } +command -v node >/dev/null 2>&1 || { echo "❌ Node.js required."; exit 1; } + +# 1) Find userRoutes path +banner "Detecting userRoutes route file" +USER_ROUTES_PATH="" +for p in \ + "src/api/routes/userRoutes.ts" \ + "src/api/routes/users.ts" \ + "src/api/routes/user.routes.ts" \ + "src/routes/userRoutes.ts" \ + "src/routes/users.ts" +do + [ -f "$p" ] && USER_ROUTES_PATH="$p" && break +done +if [ -z "$USER_ROUTES_PATH" ]; then + CANDIDATES=$(git ls-files | grep -Ei '^src/.*/routes/.*user.*\.(ts|js)$' || true) + [ -n "$CANDIDATES" ] && USER_ROUTES_PATH=$(printf "%s\n" "$CANDIDATES" | head -n1 || true) +fi +[ -z "$USER_ROUTES_PATH" ] && { echo "❌ Could not find userRoutes under src/**/routes."; exit 1; } +echo "✅ Found: $USER_ROUTES_PATH" + +# 2) Ensure dev deps (jest + ts-jest + supertest + types) +banner "Ensuring devDependencies" +has_dev() { node -e "console.log(!!(require('./package.json').devDependencies||{})['$1'])"; } +NEED=false +for d in jest ts-jest supertest @types/jest @types/supertest; do + [ "$(has_dev "$d")" = "true" ] || NEED=true +done +if $NEED; then + npm pkg set "devDependencies.jest=^29.7.0" >/dev/null 2>&1 || true + npm pkg set "devDependencies.ts-jest=^29.2.5" >/dev/null 2>&1 || true + npm pkg set "devDependencies.supertest=^7.0.0" >/dev/null 2>&1 || true + npm pkg set "devDependencies.@types/jest=^29.5.13" >/dev/null 2>&1 || true + npm pkg set "devDependencies.@types/supertest=^2.0.16" >/dev/null 2>&1 || true + npm install +else + echo "✅ Dev deps already present." +fi + +# 3) Ensure jest.config.js (non-invasive) +banner "Checking jest.config.js" +if [ ! -f jest.config.js ] && [ ! -f jest.config.cjs ] && [ ! -f jest.config.mjs ]; then + cat > jest.config.js <<'EOF' +/** Minimal Jest config for TS */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + clearMocks: true, + coverageDirectory: 'coverage', + collectCoverageFrom: [ + 'src/**/*.{ts,js}', + '!src/**/index.ts', + '!src/**/types.ts', + ], +}; +EOF + echo "🆕 Created jest.config.js" +else + echo "✅ Found existing Jest config" +fi + +# 4) Ensure npm test script +banner "Ensuring npm test script" +if ! npm pkg get scripts.test | grep -qv null; then + npm pkg set "scripts.test=jest --runInBand" + echo "🆕 Added npm test script" +else + echo "✅ test script exists" +fi + +# 5) Remove buggy helper folder if present +banner "Cleaning old helpers" +if [ -d "__tests__/helpers" ]; then + rm -rf __tests__/helpers + echo "🧹 Removed __tests__/helpers (to avoid Jest picking it up as tests)" +else + echo "✅ No __tests__/helpers to clean" +fi + +# 6) Create safe helper outside __tests__ +banner "Writing test/utils/testServer.ts" +mkdir -p test/utils + +ROUTER_IMPORT_REL="${USER_ROUTES_PATH#src/}" # e.g. api/routes/userRoutes.ts +ROUTER_IMPORT_PATH='../../src/'"$ROUTER_IMPORT_REL" # from test/utils -> src/... + +cat > test/utils/testServer.ts <<'EOF' +import express, { type RequestHandler } from 'express'; + +// IMPORTANT: this import is replaced below with your actual path +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import userRoutes from 'REPLACEME_USER_ROUTES'; + +export const BASE = `/api/${process.env.API_VERSION || 'v1'}/users`; + +const notFound: RequestHandler = (_req, res): void => { + res.status(404).json({ error: 'not_found' }); +}; + +export const makeApp = () => { + const app = express(); + app.use(express.json()); + app.use(BASE, userRoutes); + // No string path here; avoids path-to-regexp '*' issues in Express 5 + app.use(notFound); + return app; +}; + +export const http = () => { + const app = makeApp(); + // lazy require to keep types optional + // eslint-disable-next-line @typescript-eslint/no-var-requires + const request = require('supertest') as typeof import('supertest'); + return request(app); +}; +EOF + +# Replace placeholder import with the real path +# Try GNU sed first, then fallback to perl (macOS) +sed -i 's#REPLACEME_USER_ROUTES#'"$ROUTER_IMPORT_PATH"'#' test/utils/testServer.ts 2>/dev/null || \ +perl -0777 -pe 's/REPLACEME_USER_ROUTES/'"$(printf '%s' "$ROUTER_IMPORT_PATH" | sed 's/[\/&]/\\&/g')"'/g' -i test/utils/testServer.ts + +echo "✅ Helper created at test/utils/testServer.ts (imports $ROUTER_IMPORT_PATH)" + +# 7) Overwrite userRoutes test to import the new helper +banner "Writing __tests__/api/routes/userRoutes.test.ts" +mkdir -p __tests__/api/routes +cat > __tests__/api/routes/userRoutes.test.ts <<'EOF' +import { http, BASE } from '../../../test/utils/testServer'; + +describe('userRoutes', () => { + it('mounts and returns 404/401/403 for an unknown subroute (no 5xx)', async () => { + const res = await http().get(`${BASE}/__unknown__`); + expect(res.status).toBeGreaterThanOrEqual(401); // could be 401/403/404 + expect(res.status).toBeLessThan(500); // must not 5xx + }); +}); +EOF +echo "✅ Test written at __tests__/api/routes/userRoutes.test.ts" + +# 8) Run tests +banner "Running tests" +npm test --silent || { + echo "⚠️ If failures reference auth/DB middlewares on real endpoints, this probe test still passes as long as the router mounts without 5xx." + exit 1 +} + +banner "✅ Done. Coverage at ./coverage" diff --git a/test/utils/testServer.ts b/test/utils/testServer.ts new file mode 100644 index 0000000..9144642 --- /dev/null +++ b/test/utils/testServer.ts @@ -0,0 +1,29 @@ +import express, { type RequestHandler } from 'express'; + +// IMPORTANT: this import is replaced below with your actual path +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import userRoutes from '../../src/api/routes/userRoutes.ts'; + +export const BASE = `/api/${process.env.API_VERSION || 'v1'}/users`; + +const notFound: RequestHandler = (_req, res): void => { + res.status(404).json({ error: 'not_found' }); +}; + +export const makeApp = () => { + const app = express(); + app.use(express.json()); + app.use(BASE, userRoutes); + // No string path here; avoids path-to-regexp '*' issues in Express 5 + app.use(notFound); + return app; +}; + +export const http = () => { + const app = makeApp(); + // lazy require to keep types optional + // eslint-disable-next-line @typescript-eslint/no-var-requires + const request = require('supertest') as typeof import('supertest'); + return request(app); +}; From 318479c6074fa25c0e36931214110c8915f6cb1b Mon Sep 17 00:00:00 2001 From: Farrel Athaillah Date: Sun, 28 Sep 2025 19:31:00 +0700 Subject: [PATCH 2/3] Delete fix.sh --- fix.sh | 158 --------------------------------------------------------- 1 file changed, 158 deletions(-) delete mode 100755 fix.sh diff --git a/fix.sh b/fix.sh deleted file mode 100755 index 4f26391..0000000 --- a/fix.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -banner() { printf "\n\033[1;36m%s\033[0m\n" "▸ $*"; } - -# 0) Guards -[ -f package.json ] || { echo "❌ Run this from the repo root (package.json not found)."; exit 1; } -command -v node >/dev/null 2>&1 || { echo "❌ Node.js required."; exit 1; } - -# 1) Find userRoutes path -banner "Detecting userRoutes route file" -USER_ROUTES_PATH="" -for p in \ - "src/api/routes/userRoutes.ts" \ - "src/api/routes/users.ts" \ - "src/api/routes/user.routes.ts" \ - "src/routes/userRoutes.ts" \ - "src/routes/users.ts" -do - [ -f "$p" ] && USER_ROUTES_PATH="$p" && break -done -if [ -z "$USER_ROUTES_PATH" ]; then - CANDIDATES=$(git ls-files | grep -Ei '^src/.*/routes/.*user.*\.(ts|js)$' || true) - [ -n "$CANDIDATES" ] && USER_ROUTES_PATH=$(printf "%s\n" "$CANDIDATES" | head -n1 || true) -fi -[ -z "$USER_ROUTES_PATH" ] && { echo "❌ Could not find userRoutes under src/**/routes."; exit 1; } -echo "✅ Found: $USER_ROUTES_PATH" - -# 2) Ensure dev deps (jest + ts-jest + supertest + types) -banner "Ensuring devDependencies" -has_dev() { node -e "console.log(!!(require('./package.json').devDependencies||{})['$1'])"; } -NEED=false -for d in jest ts-jest supertest @types/jest @types/supertest; do - [ "$(has_dev "$d")" = "true" ] || NEED=true -done -if $NEED; then - npm pkg set "devDependencies.jest=^29.7.0" >/dev/null 2>&1 || true - npm pkg set "devDependencies.ts-jest=^29.2.5" >/dev/null 2>&1 || true - npm pkg set "devDependencies.supertest=^7.0.0" >/dev/null 2>&1 || true - npm pkg set "devDependencies.@types/jest=^29.5.13" >/dev/null 2>&1 || true - npm pkg set "devDependencies.@types/supertest=^2.0.16" >/dev/null 2>&1 || true - npm install -else - echo "✅ Dev deps already present." -fi - -# 3) Ensure jest.config.js (non-invasive) -banner "Checking jest.config.js" -if [ ! -f jest.config.js ] && [ ! -f jest.config.cjs ] && [ ! -f jest.config.mjs ]; then - cat > jest.config.js <<'EOF' -/** Minimal Jest config for TS */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/__tests__/**/*.test.ts'], - moduleFileExtensions: ['ts', 'js', 'json'], - clearMocks: true, - coverageDirectory: 'coverage', - collectCoverageFrom: [ - 'src/**/*.{ts,js}', - '!src/**/index.ts', - '!src/**/types.ts', - ], -}; -EOF - echo "🆕 Created jest.config.js" -else - echo "✅ Found existing Jest config" -fi - -# 4) Ensure npm test script -banner "Ensuring npm test script" -if ! npm pkg get scripts.test | grep -qv null; then - npm pkg set "scripts.test=jest --runInBand" - echo "🆕 Added npm test script" -else - echo "✅ test script exists" -fi - -# 5) Remove buggy helper folder if present -banner "Cleaning old helpers" -if [ -d "__tests__/helpers" ]; then - rm -rf __tests__/helpers - echo "🧹 Removed __tests__/helpers (to avoid Jest picking it up as tests)" -else - echo "✅ No __tests__/helpers to clean" -fi - -# 6) Create safe helper outside __tests__ -banner "Writing test/utils/testServer.ts" -mkdir -p test/utils - -ROUTER_IMPORT_REL="${USER_ROUTES_PATH#src/}" # e.g. api/routes/userRoutes.ts -ROUTER_IMPORT_PATH='../../src/'"$ROUTER_IMPORT_REL" # from test/utils -> src/... - -cat > test/utils/testServer.ts <<'EOF' -import express, { type RequestHandler } from 'express'; - -// IMPORTANT: this import is replaced below with your actual path -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import userRoutes from 'REPLACEME_USER_ROUTES'; - -export const BASE = `/api/${process.env.API_VERSION || 'v1'}/users`; - -const notFound: RequestHandler = (_req, res): void => { - res.status(404).json({ error: 'not_found' }); -}; - -export const makeApp = () => { - const app = express(); - app.use(express.json()); - app.use(BASE, userRoutes); - // No string path here; avoids path-to-regexp '*' issues in Express 5 - app.use(notFound); - return app; -}; - -export const http = () => { - const app = makeApp(); - // lazy require to keep types optional - // eslint-disable-next-line @typescript-eslint/no-var-requires - const request = require('supertest') as typeof import('supertest'); - return request(app); -}; -EOF - -# Replace placeholder import with the real path -# Try GNU sed first, then fallback to perl (macOS) -sed -i 's#REPLACEME_USER_ROUTES#'"$ROUTER_IMPORT_PATH"'#' test/utils/testServer.ts 2>/dev/null || \ -perl -0777 -pe 's/REPLACEME_USER_ROUTES/'"$(printf '%s' "$ROUTER_IMPORT_PATH" | sed 's/[\/&]/\\&/g')"'/g' -i test/utils/testServer.ts - -echo "✅ Helper created at test/utils/testServer.ts (imports $ROUTER_IMPORT_PATH)" - -# 7) Overwrite userRoutes test to import the new helper -banner "Writing __tests__/api/routes/userRoutes.test.ts" -mkdir -p __tests__/api/routes -cat > __tests__/api/routes/userRoutes.test.ts <<'EOF' -import { http, BASE } from '../../../test/utils/testServer'; - -describe('userRoutes', () => { - it('mounts and returns 404/401/403 for an unknown subroute (no 5xx)', async () => { - const res = await http().get(`${BASE}/__unknown__`); - expect(res.status).toBeGreaterThanOrEqual(401); // could be 401/403/404 - expect(res.status).toBeLessThan(500); // must not 5xx - }); -}); -EOF -echo "✅ Test written at __tests__/api/routes/userRoutes.test.ts" - -# 8) Run tests -banner "Running tests" -npm test --silent || { - echo "⚠️ If failures reference auth/DB middlewares on real endpoints, this probe test still passes as long as the router mounts without 5xx." - exit 1 -} - -banner "✅ Done. Coverage at ./coverage" From 401eade8a392dd471b9489620f607de12611dcb3 Mon Sep 17 00:00:00 2001 From: Farrel Athaillah Date: Sun, 28 Sep 2025 16:14:44 +0000 Subject: [PATCH 3/3] resolve the problem --- __tests__/api/routes/userRoutes.test.ts | 141 ++++++++++++++++++- {test => __tests__/test}/utils/testServer.ts | 9 +- 2 files changed, 136 insertions(+), 14 deletions(-) rename {test => __tests__/test}/utils/testServer.ts (58%) diff --git a/__tests__/api/routes/userRoutes.test.ts b/__tests__/api/routes/userRoutes.test.ts index 9610445..57dc5a6 100644 --- a/__tests__/api/routes/userRoutes.test.ts +++ b/__tests__/api/routes/userRoutes.test.ts @@ -1,9 +1,138 @@ -import { http, BASE } from '../../../test/utils/testServer'; +import express from 'express'; +import request from 'supertest'; -describe('userRoutes', () => { - it('mounts and returns 404/401/403 for an unknown subroute (no 5xx)', async () => { - const res = await http().get(`${BASE}/__unknown__`); - expect(res.status).toBeGreaterThanOrEqual(401); // could be 401/403/404 - expect(res.status).toBeLessThan(500); // must not 5xx +// Mock controller handlers to simple 200 responses we can assert +jest.mock('../../../src/api/controllers/userController', () => ({ + getUserProfile: jest.fn((req, res) => res.status(200).json({ route: 'profile' })), + getUserOwnedThemes: jest.fn((req, res) => res.status(200).json({ route: 'ownedThemes' })), + getUserFavoriteThemes: jest.fn((req, res) => res.status(200).json({ route: 'favoriteThemes' })), + addUserFavoriteTheme: jest.fn((req, res) => res.status(200).json({ route: 'addFavoriteTheme' })), + removeUserFavoriteTheme: jest.fn((req, res) => res.status(200).json({ route: 'removeFavoriteTheme' })), + getUserOwnedPlugins: jest.fn((req, res) => res.status(200).json({ route: 'ownedPlugins' })), + getUserFavoritePlugins: jest.fn((req, res) => res.status(200).json({ route: 'favoritePlugins' })), + addUserFavoritePlugin: jest.fn((req, res) => res.status(200).json({ route: 'addFavoritePlugin' })), + removeUserFavoritePlugin: jest.fn((req, res) => res.status(200).json({ route: 'removeFavoritePlugin' })), + setUserAcceptAuthorAgreement: jest.fn((req, res) => res.status(200).json({ route: 'authorAgreement' })), +})); + +// Mock session middleware to simply call next() +jest.mock('../../../src/api/middleware/userSessionMiddleware', () => ({ + __esModule: true, + default: (_req: any, _res: any, next: any) => next(), +})); + +import userRoutes from '../../../src/api/routes/userRoutes'; +import { + getUserProfile, + getUserOwnedThemes, + getUserFavoriteThemes, + addUserFavoriteTheme, + removeUserFavoriteTheme, + getUserOwnedPlugins, + getUserFavoritePlugins, + addUserFavoritePlugin, + removeUserFavoritePlugin, + setUserAcceptAuthorAgreement, +} from '../../../src/api/controllers/userController'; + +const mGetUserProfile = getUserProfile as jest.MockedFunction; +const mGetUserOwnedThemes = getUserOwnedThemes as jest.MockedFunction; +const mGetUserFavoriteThemes = getUserFavoriteThemes as jest.MockedFunction; +const mAddUserFavoriteTheme = addUserFavoriteTheme as jest.MockedFunction; +const mRemoveUserFavoriteTheme = removeUserFavoriteTheme as jest.MockedFunction; +const mGetUserOwnedPlugins = getUserOwnedPlugins as jest.MockedFunction; +const mGetUserFavoritePlugins = getUserFavoritePlugins as jest.MockedFunction; +const mAddUserFavoritePlugin = addUserFavoritePlugin as jest.MockedFunction; +const mRemoveUserFavoritePlugin = removeUserFavoritePlugin as jest.MockedFunction; +const mSetUserAcceptAuthorAgreement = setUserAcceptAuthorAgreement as jest.MockedFunction; + +describe('user routes', () => { + const API_PREFIX = `/api/${process.env.API_VERSION || 'v1'}`; + const app = express(); + + app.use(`${API_PREFIX}/users`, userRoutes); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('GET /profile -> getUserProfile', async () => { + const res = await request(app).get(`${API_PREFIX}/users/profile`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'profile' }); + expect(mGetUserProfile).toHaveBeenCalledTimes(1); + }); + + it('GET /themes -> getUserOwnedThemes', async () => { + const res = await request(app).get(`${API_PREFIX}/users/themes`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'ownedThemes' }); + expect(mGetUserOwnedThemes).toHaveBeenCalledTimes(1); + }); + + it('GET /themes/favorited -> getUserFavoriteThemes', async () => { + const res = await request(app).get(`${API_PREFIX}/users/themes/favorited`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'favoriteThemes' }); + expect(mGetUserFavoriteThemes).toHaveBeenCalledTimes(1); + }); + + it('POST /themes/favorited -> addUserFavoriteTheme', async () => { + const res = await request(app) + .post(`${API_PREFIX}/users/themes/favorited`) + .send({ themeId: 't-1' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'addFavoriteTheme' }); + expect(mAddUserFavoriteTheme).toHaveBeenCalledTimes(1); + }); + + it('DELETE /themes/favorited -> removeUserFavoriteTheme', async () => { + const res = await request(app) + .delete(`${API_PREFIX}/users/themes/favorited`) + .send({ themeId: 't-1' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'removeFavoriteTheme' }); + expect(mRemoveUserFavoriteTheme).toHaveBeenCalledTimes(1); + }); + + it('GET /plugins -> getUserOwnedPlugins', async () => { + const res = await request(app).get(`${API_PREFIX}/users/plugins`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'ownedPlugins' }); + expect(mGetUserOwnedPlugins).toHaveBeenCalledTimes(1); + }); + + it('GET /plugins/favorited -> getUserFavoritePlugins', async () => { + const res = await request(app).get(`${API_PREFIX}/users/plugins/favorited`); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'favoritePlugins' }); + expect(mGetUserFavoritePlugins).toHaveBeenCalledTimes(1); + }); + + it('POST /plugins/favorited -> addUserFavoritePlugin', async () => { + const res = await request(app) + .post(`${API_PREFIX}/users/plugins/favorited`) + .send({ pluginId: 'p-1' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'addFavoritePlugin' }); + expect(mAddUserFavoritePlugin).toHaveBeenCalledTimes(1); + }); + + it('DELETE /plugins/favorited -> removeUserFavoritePlugin', async () => { + const res = await request(app) + .delete(`${API_PREFIX}/users/plugins/favorited`) + .send({ pluginId: 'p-1' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'removeFavoritePlugin' }); + expect(mRemoveUserFavoritePlugin).toHaveBeenCalledTimes(1); + }); + + it('POST /author-agreement -> setUserAcceptAuthorAgreement', async () => { + const res = await request(app) + .post(`${API_PREFIX}/users/author-agreement`) + .send({ accept: true }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ route: 'authorAgreement' }); + expect(mSetUserAcceptAuthorAgreement).toHaveBeenCalledTimes(1); }); }); diff --git a/test/utils/testServer.ts b/__tests__/test/utils/testServer.ts similarity index 58% rename from test/utils/testServer.ts rename to __tests__/test/utils/testServer.ts index 9144642..9bf212a 100644 --- a/test/utils/testServer.ts +++ b/__tests__/test/utils/testServer.ts @@ -1,9 +1,5 @@ import express, { type RequestHandler } from 'express'; - -// IMPORTANT: this import is replaced below with your actual path -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import userRoutes from '../../src/api/routes/userRoutes.ts'; +import userRoutes from '../../../src/api/routes/userRoutes'; export const BASE = `/api/${process.env.API_VERSION || 'v1'}/users`; @@ -15,15 +11,12 @@ export const makeApp = () => { const app = express(); app.use(express.json()); app.use(BASE, userRoutes); - // No string path here; avoids path-to-regexp '*' issues in Express 5 app.use(notFound); return app; }; export const http = () => { const app = makeApp(); - // lazy require to keep types optional - // eslint-disable-next-line @typescript-eslint/no-var-requires const request = require('supertest') as typeof import('supertest'); return request(app); };