From 733becc756b528204054330151297ee3622ba662 Mon Sep 17 00:00:00 2001 From: sublime247 Date: Mon, 30 Mar 2026 10:32:18 +0100 Subject: [PATCH 1/3] feat(request-id-middleware) --- api/src/app.ts | 3 + .../middleware/__tests__/requestId.test.ts | 53 ++++++++ api/src/middleware/requestId.ts | 14 +++ api/src/types/express.d.ts | 7 ++ api/src/utils/logger.ts | 12 ++ package-lock.json | 118 ++---------------- 6 files changed, 99 insertions(+), 108 deletions(-) create mode 100644 api/src/middleware/__tests__/requestId.test.ts create mode 100644 api/src/middleware/requestId.ts create mode 100644 api/src/types/express.d.ts diff --git a/api/src/app.ts b/api/src/app.ts index 06a407e6..73e3e5ce 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -11,8 +11,11 @@ import { errorHandler } from './middleware/errorHandler'; import { idempotencyMiddleware } from './middleware/idempotency'; import { swaggerSpec } from './config/swagger'; import logger from './utils/logger'; +import { requestIdMiddleware } from './middleware/requestId'; const app: Application = express(); +app.use(requestIdMiddleware); + const ipRateLimitStore = new MemoryStore(); const userRateLimitStore = new MemoryStore(); diff --git a/api/src/middleware/__tests__/requestId.test.ts b/api/src/middleware/__tests__/requestId.test.ts new file mode 100644 index 00000000..0b544b90 --- /dev/null +++ b/api/src/middleware/__tests__/requestId.test.ts @@ -0,0 +1,53 @@ +import { Request, Response, NextFunction } from 'express'; +import { requestIdMiddleware } from '../requestId'; +import { requestContext } from '../../utils/logger'; + +describe('requestIdMiddleware', () => { + let mockRequest: Partial; + let mockResponse: Partial; + let nextFunction: NextFunction; + + beforeEach(() => { + mockRequest = { + headers: {}, + }; + mockResponse = { + setHeader: jest.fn(), + }; + nextFunction = jest.fn(); + }); + + it('should generate a new UUID if x-request-id header is not present', () => { + requestIdMiddleware(mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockRequest.id).toBeDefined(); + expect(typeof mockRequest.id).toBe('string'); + expect(mockRequest.id?.length).toBeGreaterThan(0); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-request-id', mockRequest.id); + expect(nextFunction).toHaveBeenCalled(); + }); + + it('should use incoming x-request-id header if present', () => { + mockRequest.headers = { 'x-request-id': 'test-req-id' }; + + requestIdMiddleware(mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockRequest.id).toBe('test-req-id'); + expect(mockResponse.setHeader).toHaveBeenCalledWith('x-request-id', 'test-req-id'); + expect(nextFunction).toHaveBeenCalled(); + }); + + it('should run next function within async local storage context', (done) => { + nextFunction = jest.fn(() => { + try { + const storeId = requestContext.getStore(); + expect(storeId).toBe(mockRequest.id); + done(); + } catch (err) { + done(err); + } + }); + + requestIdMiddleware(mockRequest as Request, mockResponse as Response, nextFunction); + }); +}); diff --git a/api/src/middleware/requestId.ts b/api/src/middleware/requestId.ts new file mode 100644 index 00000000..43818136 --- /dev/null +++ b/api/src/middleware/requestId.ts @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express'; +import { randomUUID } from 'crypto'; +import { requestContext } from '../utils/logger'; + +export const requestIdMiddleware = (req: Request, res: Response, next: NextFunction) => { + const reqId = req.headers['x-request-id']; + req.id = (Array.isArray(reqId) ? reqId[0] : reqId) || randomUUID(); + res.setHeader('x-request-id', req.id); + + // Set the request ID in the async local storage context for logger propagation + requestContext.run(req.id, () => { + next(); + }); +}; diff --git a/api/src/types/express.d.ts b/api/src/types/express.d.ts new file mode 100644 index 00000000..4106ddbc --- /dev/null +++ b/api/src/types/express.d.ts @@ -0,0 +1,7 @@ +import 'express'; + +declare module 'express-serve-static-core' { + interface Request { + id: string; + } +} diff --git a/api/src/utils/logger.ts b/api/src/utils/logger.ts index a5018411..af5cf782 100644 --- a/api/src/utils/logger.ts +++ b/api/src/utils/logger.ts @@ -1,9 +1,21 @@ import winston from 'winston'; +import { AsyncLocalStorage } from 'async_hooks'; import { config } from '../config'; +export const requestContext = new AsyncLocalStorage(); + +const addRequestId = winston.format((info: winston.Logform.TransformableInfo) => { + const reqId = requestContext.getStore(); + if (reqId) { + info.requestId = reqId; + } + return info; +}); + const logger = winston.createLogger({ level: config.logging.level, format: winston.format.combine( + addRequestId(), winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() diff --git a/package-lock.json b/package-lock.json index d40384ac..ab9367cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,23 @@ { "name": "stellarlend", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stellarlend", + "version": "1.0.0", "workspaces": [ "api", "oracle" ], + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.3.3" + }, "engines": { - "node": ">=18.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" } }, "api": { @@ -3354,50 +3361,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/bare-addon-resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz", - "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-module-resolve": "^1.10.0", - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-module-resolve": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz", - "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-semver": "^1.0.0" - }, - "peerDependencies": { - "bare-url": "*" - }, - "peerDependenciesMeta": { - "bare-url": { - "optional": true - } - } - }, - "node_modules/bare-semver": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", - "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/base32.js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", @@ -7803,19 +7766,6 @@ "node": ">=4" } }, - "node_modules/require-addon": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", - "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-addon-resolve": "^1.3.0" - }, - "engines": { - "bare": ">=1.10.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8267,16 +8217,6 @@ "node": ">=8" } }, - "node_modules/sodium-native": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", - "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==", - "license": "MIT", - "optional": true, - "dependencies": { - "require-addon": "^1.1.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9053,12 +8993,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10305,7 +10239,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@stellar/stellar-sdk": "^12.0.0", + "@stellar/stellar-sdk": "^14.5.0", "axios": "^1.6.0", "dotenv": "^16.3.0", "ioredis": "^5.3.0", @@ -10324,39 +10258,7 @@ "vitest": "^1.0.0" }, "engines": { - "node": ">=18.0.0" - } - }, - "oracle/node_modules/@stellar/stellar-base": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-12.1.1.tgz", - "integrity": "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "optionalDependencies": { - "sodium-native": "^4.1.1" - } - }, - "oracle/node_modules/@stellar/stellar-sdk": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-12.3.0.tgz", - "integrity": "sha512-F2DYFop/M5ffXF0lvV5Ezjk+VWNKg0QDX8gNhwehVU3y5LYA3WAY6VcCarMGPaG9Wdgoeh1IXXzOautpqpsltw==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^12.1.1", - "axios": "^1.7.7", - "bignumber.js": "^9.1.2", - "eventsource": "^2.0.2", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" + "node": ">=20.0.0" } } } From 0c7b0f9fbf8515a4f969a719a801b550a018e0eb Mon Sep 17 00:00:00 2001 From: sublime247 Date: Mon, 30 Mar 2026 10:47:16 +0100 Subject: [PATCH 2/3] finilized --- api/src/__tests__/auth.test.ts | 2 +- api/src/__tests__/bodySizeLimit.test.ts | 10 ++++++++-- api/src/controllers/lending.controller.ts | 15 +++++++++++++++ api/src/middleware/__tests__/requestId.test.ts | 2 +- api/src/middleware/requestId.ts | 2 +- api/src/utils/errors.ts | 8 +++++++- api/src/utils/logger.ts | 4 +--- api/src/utils/requestContext.ts | 3 +++ 8 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 api/src/utils/requestContext.ts diff --git a/api/src/__tests__/auth.test.ts b/api/src/__tests__/auth.test.ts index e17febb4..6277e7f1 100644 --- a/api/src/__tests__/auth.test.ts +++ b/api/src/__tests__/auth.test.ts @@ -36,7 +36,7 @@ describe('Auth Middleware', () => { authenticateToken(mockRequest as AuthRequest, mockResponse as Response, mockNext); expect(mockNext).toHaveBeenCalledWith(); - expect(mockRequest.user).toEqual({ address: '0x1234567890123456789012345678901234567890' }); + expect(mockRequest.user).toMatchObject({ address: '0x1234567890123456789012345678901234567890' }); }); it('should return 401 when token is missing', () => { diff --git a/api/src/__tests__/bodySizeLimit.test.ts b/api/src/__tests__/bodySizeLimit.test.ts index 1b9b3353..6eb0f6ff 100644 --- a/api/src/__tests__/bodySizeLimit.test.ts +++ b/api/src/__tests__/bodySizeLimit.test.ts @@ -15,11 +15,17 @@ describe('Body Size Limit Middleware', () => { res.status(200).json({ success: true }); }); testApp.use(errorHandler); - // Restore original limit - config.bodySizeLimit.limit = originalLimit; + // Don't restore limit here because the request executes asynchronously later. + // Instead we'll manage it per-test or use a wrapper. return testApp; }; + const originalLimit = config.bodySizeLimit.limit; + + afterEach(() => { + config.bodySizeLimit.limit = originalLimit; + }); + describe('bodySizeLimitMiddleware', () => { it('should allow requests with body size within limit', async () => { const app = createTestApp('10kb'); diff --git a/api/src/controllers/lending.controller.ts b/api/src/controllers/lending.controller.ts index a6bd6688..aaa2303f 100644 --- a/api/src/controllers/lending.controller.ts +++ b/api/src/controllers/lending.controller.ts @@ -100,3 +100,18 @@ export const protocolStats = async (_req: Request, res: Response, next: NextFunc next(error); } }; + +export const getTransactionHistory = async (req: Request, res: Response, next: NextFunction) => { + try { + const { userAddress } = req.params; + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; + const cursor = req.query.cursor as string | undefined; + + const stellarService = new StellarService(); + const history = await stellarService.getTransactionHistory({ userAddress, limit, cursor }); + + return res.status(200).json(history); + } catch (error) { + next(error); + } +}; diff --git a/api/src/middleware/__tests__/requestId.test.ts b/api/src/middleware/__tests__/requestId.test.ts index 0b544b90..f283a82a 100644 --- a/api/src/middleware/__tests__/requestId.test.ts +++ b/api/src/middleware/__tests__/requestId.test.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { requestIdMiddleware } from '../requestId'; -import { requestContext } from '../../utils/logger'; +import { requestContext } from '../../utils/requestContext'; describe('requestIdMiddleware', () => { let mockRequest: Partial; diff --git a/api/src/middleware/requestId.ts b/api/src/middleware/requestId.ts index 43818136..887f9b93 100644 --- a/api/src/middleware/requestId.ts +++ b/api/src/middleware/requestId.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { randomUUID } from 'crypto'; -import { requestContext } from '../utils/logger'; +import { requestContext } from '../utils/requestContext'; export const requestIdMiddleware = (req: Request, res: Response, next: NextFunction) => { const reqId = req.headers['x-request-id']; diff --git a/api/src/utils/errors.ts b/api/src/utils/errors.ts index 0e2e8517..fa828739 100644 --- a/api/src/utils/errors.ts +++ b/api/src/utils/errors.ts @@ -25,35 +25,41 @@ export class ApiError extends Error { export class ValidationError extends ApiError { constructor(message: string) { super(400, message, ErrorCode.VALIDATION_ERROR); + Object.setPrototypeOf(this, ValidationError.prototype); } } export class UnauthorizedError extends ApiError { constructor(message = 'Unauthorized') { super(401, message, ErrorCode.UNAUTHORIZED); + Object.setPrototypeOf(this, UnauthorizedError.prototype); } } export class NotFoundError extends ApiError { constructor(message = 'Resource not found') { super(404, message, ErrorCode.NOT_FOUND); + Object.setPrototypeOf(this, NotFoundError.prototype); } } export class ConflictError extends ApiError { constructor(message: string) { super(409, message, ErrorCode.CONFLICT); + Object.setPrototypeOf(this, ConflictError.prototype); } } export class PayloadTooLargeError extends ApiError { constructor(message = 'Request body too large') { - super(413, message); + super(413, message, ErrorCode.VALIDATION_ERROR); + Object.setPrototypeOf(this, PayloadTooLargeError.prototype); } } export class InternalServerError extends ApiError { constructor(message = 'Internal server error') { super(500, message, ErrorCode.INTERNAL_SERVER_ERROR); + Object.setPrototypeOf(this, InternalServerError.prototype); } } diff --git a/api/src/utils/logger.ts b/api/src/utils/logger.ts index af5cf782..93cd217f 100644 --- a/api/src/utils/logger.ts +++ b/api/src/utils/logger.ts @@ -1,8 +1,6 @@ import winston from 'winston'; -import { AsyncLocalStorage } from 'async_hooks'; import { config } from '../config'; - -export const requestContext = new AsyncLocalStorage(); +import { requestContext } from './requestContext'; const addRequestId = winston.format((info: winston.Logform.TransformableInfo) => { const reqId = requestContext.getStore(); diff --git a/api/src/utils/requestContext.ts b/api/src/utils/requestContext.ts new file mode 100644 index 00000000..af9b45d9 --- /dev/null +++ b/api/src/utils/requestContext.ts @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +export const requestContext = new AsyncLocalStorage(); From 4c86b65758f84e7464716ed7613a4e0bf98ffa65 Mon Sep 17 00:00:00 2001 From: sublime247 Date: Mon, 30 Mar 2026 10:51:45 +0100 Subject: [PATCH 3/3] .. --- api/test-results.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 api/test-results.json diff --git a/api/test-results.json b/api/test-results.json new file mode 100644 index 00000000..7300416a --- /dev/null +++ b/api/test-results.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":9,"numPassedTests":126,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":9,"numTotalTests":126,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1774864272582,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":9,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should pass through with valid token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should pass through with valid token"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":8,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when token is missing","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when token is missing"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when token is expired","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when token is expired"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when token is invalid","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when token is invalid"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when authorization header is malformed","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when authorization header is malformed"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when authorization header has no token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when authorization header has no token"},{"ancestorTitles":["Auth Middleware","authenticateToken"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Auth Middleware authenticateToken should return 401 when authorization header has only Bearer without space","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 401 when authorization header has only Bearer without space"}],"endTime":1774864273430,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/auth.test.ts","startTime":1774864273065,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Error Handler Middleware"],"duration":12,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle ValidationError with correct status code and error code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle ValidationError with correct status code and error code"},{"ancestorTitles":["Error Handler Middleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle UnauthorizedError","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle UnauthorizedError"},{"ancestorTitles":["Error Handler Middleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle NotFoundError with correct status code and error code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle NotFoundError with correct status code and error code"},{"ancestorTitles":["Error Handler Middleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle ConflictError with correct status code and error code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle ConflictError with correct status code and error code"},{"ancestorTitles":["Error Handler Middleware"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle InternalServerError with correct status code and error code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle InternalServerError with correct status code and error code"},{"ancestorTitles":["Error Handler Middleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle generic errors with 500 status and INTERNAL_SERVER_ERROR code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle generic errors with 500 status and INTERNAL_SERVER_ERROR code"},{"ancestorTitles":["Error Handler Middleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handler Middleware should handle SyntaxError with 400 status and VALIDATION_ERROR code","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle SyntaxError with 400 status and VALIDATION_ERROR code"}],"endTime":1774864273497,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/errorHandler.test.ts","startTime":1774864273084,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Config Validation"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Config Validation should throw an error if CONTRACT_ID is missing","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw an error if CONTRACT_ID is missing"},{"ancestorTitles":["Config Validation"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Config Validation should not throw an error if CONTRACT_ID and secure JWT_SECRET are present","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should not throw an error if CONTRACT_ID and secure JWT_SECRET are present"},{"ancestorTitles":["Config Validation"],"duration":7,"failureDetails":[],"failureMessages":[],"fullName":"Config Validation should throw an error if JWT_SECRET is missing","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw an error if JWT_SECRET is missing"},{"ancestorTitles":["Config Validation"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Config Validation should throw an error if JWT_SECRET is the default insecure value","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw an error if JWT_SECRET is the default insecure value"},{"ancestorTitles":["Config Validation"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Config Validation should throw an error if JWT_SECRET is too short","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw an error if JWT_SECRET is too short"}],"endTime":1774864273501,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/config.test.ts","startTime":1774864273437,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["requestIdMiddleware"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"requestIdMiddleware should generate a new UUID if x-request-id header is not present","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"status":"passed","title":"should generate a new UUID if x-request-id header is not present"},{"ancestorTitles":["requestIdMiddleware"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"requestIdMiddleware should use incoming x-request-id header if present","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should use incoming x-request-id header if present"},{"ancestorTitles":["requestIdMiddleware"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"requestIdMiddleware should run next function within async local storage context","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should run next function within async local storage context"}],"endTime":1774864273544,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/middleware/__tests__/requestId.test.ts","startTime":1774864273504,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Body Size Limit Middleware","bodySizeLimitMiddleware"],"duration":31,"failureDetails":[],"failureMessages":[],"fullName":"Body Size Limit Middleware bodySizeLimitMiddleware should allow requests with body size within limit","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should allow requests with body size within limit"},{"ancestorTitles":["Body Size Limit Middleware","bodySizeLimitMiddleware"],"duration":15,"failureDetails":[],"failureMessages":[],"fullName":"Body Size Limit Middleware bodySizeLimitMiddleware should return 413 when Content-Length exceeds limit","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should return 413 when Content-Length exceeds limit"},{"ancestorTitles":["Body Size Limit Middleware","bodySizeLimitMiddleware"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Body Size Limit Middleware bodySizeLimitMiddleware should throw PayloadTooLargeError when body size exceeds configured limit","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw PayloadTooLargeError when body size exceeds configured limit"},{"ancestorTitles":["Body Size Limit Middleware","parseSizeLimit helper"],"duration":9,"failureDetails":[],"failureMessages":[],"fullName":"Body Size Limit Middleware parseSizeLimit helper should handle different size units","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should handle different size units"}],"endTime":1774864273689,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/bodySizeLimit.test.ts","startTime":1774864273056,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":29,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return unsigned XDR for deposit","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"should return unsigned XDR for deposit"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return unsigned XDR for borrow","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"should return unsigned XDR for borrow"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return unsigned XDR for repay","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"should return unsigned XDR for repay"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return unsigned XDR for withdraw","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"should return unsigned XDR for withdraw"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return 400 for invalid operation","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should return 400 for invalid operation"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return 400 for missing userAddress","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should return 400 for missing userAddress"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should return 400 for zero amount","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should return 400 for zero amount"},{"ancestorTitles":["Lending Controller","GET /api/lending/prepare/:operation"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/lending/prepare/:operation should not accept userSecret in request body","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should not accept userSecret in request body"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should submit signed XDR and return transaction result","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should submit signed XDR and return transaction result"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should log audit entry when transaction succeeds with full audit data","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should log audit entry when transaction succeeds with full audit data"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should log audit entry with redacted data when audit fields are missing","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should log audit entry with redacted data when audit fields are missing"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should validate optional audit fields when provided","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should validate optional audit fields when provided"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should return 400 when transaction fails","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should return 400 when transaction fails"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should return 400 when signedXdr is missing","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should return 400 when signedXdr is missing"},{"ancestorTitles":["Lending Controller","POST /api/lending/submit"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller POST /api/lending/submit should never log secrets in audit entries","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"status":"passed","title":"should never log secrets in audit entries"},{"ancestorTitles":["Lending Controller","GET /api/health"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/health should return healthy status when all services are up","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return healthy status when all services are up"},{"ancestorTitles":["Lending Controller","GET /api/health"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/health should return unhealthy status when services are down","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return unhealthy status when services are down"},{"ancestorTitles":["Lending Controller","GET /api/health/live"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/health/live should return ok without checking upstream dependencies","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should return ok without checking upstream dependencies"},{"ancestorTitles":["Lending Controller","GET /api/health/ready"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/health/ready should return ok when all dependencies are up","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return ok when all dependencies are up"},{"ancestorTitles":["Lending Controller","GET /api/health/ready"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/health/ready should return 503 when a dependency is unavailable","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return 503 when a dependency is unavailable"},{"ancestorTitles":["Lending Controller","GET /api/protocol/stats"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller GET /api/protocol/stats should return protocol statistics","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should return protocol statistics"},{"ancestorTitles":["Lending Controller","Idempotency-Key"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller Idempotency-Key should replay a cached submit response for duplicate POST requests","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"status":"passed","title":"should replay a cached submit response for duplicate POST requests"},{"ancestorTitles":["Lending Controller","Idempotency-Key"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Lending Controller Idempotency-Key should reject non-UUID idempotency keys","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject non-UUID idempotency keys"}],"endTime":1774864274166,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/lending.controller.test.ts","startTime":1774864273104,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Complete Deposit Flow"],"duration":22,"failureDetails":[],"failureMessages":[],"fullName":"Complete Deposit Flow prepare returns unsigned XDR with correct shape","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"prepare returns unsigned XDR with correct shape"},{"ancestorTitles":["Complete Deposit Flow"],"duration":7,"failureDetails":[],"failureMessages":[],"fullName":"Complete Deposit Flow prepare calls buildUnsignedTransaction with correct args","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"prepare calls buildUnsignedTransaction with correct args"},{"ancestorTitles":["Complete Deposit Flow"],"duration":8,"failureDetails":[],"failureMessages":[],"fullName":"Complete Deposit Flow submit returns success with transaction hash and ledger","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"submit returns success with transaction hash and ledger"},{"ancestorTitles":["Complete Deposit Flow"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Complete Deposit Flow submit calls monitorTransaction after successful submitTransaction","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"submit calls monitorTransaction after successful submitTransaction"},{"ancestorTitles":["Complete Deposit Flow"],"duration":6,"failureDetails":[],"failureMessages":[],"fullName":"Complete Deposit Flow full prepare → submit lifecycle returns consistent data","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"status":"passed","title":"full prepare → submit lifecycle returns consistent data"},{"ancestorTitles":["Error Handling"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 for an invalid operation name","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 400 for an invalid operation name"},{"ancestorTitles":["Error Handling"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 when userAddress is missing","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 400 when userAddress is missing"},{"ancestorTitles":["Error Handling"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 when amount is missing","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 400 when amount is missing"},{"ancestorTitles":["Error Handling"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 when userAddress is not a valid Stellar key","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 400 when userAddress is not a valid Stellar key"},{"ancestorTitles":["Error Handling"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 when signedXdr is missing on submit","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 400 when signedXdr is missing on submit"},{"ancestorTitles":["Error Handling"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 when submit receives malformed JSON","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"returns 400 when submit receives malformed JSON"},{"ancestorTitles":["Error Handling"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 500 when stellar service fails to build transaction","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"returns 500 when stellar service fails to build transaction"},{"ancestorTitles":["Error Handling"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling returns 400 from submit when submitTransaction reports failure","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"returns 400 from submit when submitTransaction reports failure"},{"ancestorTitles":["Error Handling"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling health endpoint returns 503 when services are down","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"health endpoint returns 503 when services are down"},{"ancestorTitles":["Error Handling"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling liveness endpoint returns immediately without upstream checks","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"liveness endpoint returns immediately without upstream checks"},{"ancestorTitles":["Error Handling"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Error Handling readiness endpoint returns dependency status details","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"readiness endpoint returns dependency status details"},{"ancestorTitles":["Edge Cases"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Edge Cases rejects amount of zero","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"rejects amount of zero"},{"ancestorTitles":["Edge Cases"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Edge Cases rejects negative amount","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"rejects negative amount"},{"ancestorTitles":["Edge Cases"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Edge Cases accepts optional assetAddress when provided","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"accepts optional assetAddress when provided"},{"ancestorTitles":["Edge Cases"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Edge Cases works without optional assetAddress","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"works without optional assetAddress"},{"ancestorTitles":["Edge Cases"],"duration":8,"failureDetails":[],"failureMessages":[],"fullName":"Edge Cases all four valid operations are accepted by prepare","invocations":1,"location":null,"numPassingAsserts":8,"retryReasons":[],"status":"passed","title":"all four valid operations are accepted by prepare"},{"ancestorTitles":["Protocol Stats"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Protocol Stats returns protocol statistics with cache headers","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"returns protocol statistics with cache headers"},{"ancestorTitles":["Idempotency"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Idempotency replays cached submit responses for duplicate POST requests","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"status":"passed","title":"replays cached submit responses for duplicate POST requests"},{"ancestorTitles":["Idempotency"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Idempotency rejects invalid idempotency keys","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"rejects invalid idempotency keys"},{"ancestorTitles":["Security Headers"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Security Headers includes x-content-type-options header","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"includes x-content-type-options header"},{"ancestorTitles":["Security Headers"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Security Headers includes x-frame-options header","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"includes x-frame-options header"},{"ancestorTitles":["Security Headers"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Security Headers includes strict-transport-security header","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"includes strict-transport-security header"},{"ancestorTitles":["Security Headers"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Security Headers responds to OPTIONS preflight requests","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"responds to OPTIONS preflight requests"},{"ancestorTitles":["Security Headers"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"Security Headers health endpoint returns healthy status with correct shape","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"health endpoint returns healthy status with correct shape"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":14,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting allows different users to make requests independently","invocations":1,"location":null,"numPassingAsserts":10,"retryReasons":[],"status":"passed","title":"allows different users to make requests independently"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":11,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting enforces per-user rate limit for requests with userAddress in query params","invocations":1,"location":null,"numPassingAsserts":12,"retryReasons":[],"status":"passed","title":"enforces per-user rate limit for requests with userAddress in query params"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":14,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting enforces per-user rate limit for requests with userAddress in request body","invocations":1,"location":null,"numPassingAsserts":12,"retryReasons":[],"status":"passed","title":"enforces per-user rate limit for requests with userAddress in request body"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":6,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting falls back to IP-based limiting when userAddress is not provided","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"status":"passed","title":"falls back to IP-based limiting when userAddress is not provided"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":13,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting resets per-user rate limit after window expires","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"resets per-user rate limit after window expires"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":9,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting does not affect non-lending endpoints","invocations":1,"location":null,"numPassingAsserts":15,"retryReasons":[],"status":"passed","title":"does not affect non-lending endpoints"},{"ancestorTitles":["Per-User Rate Limiting"],"duration":12,"failureDetails":[],"failureMessages":[],"fullName":"Per-User Rate Limiting handles mixed userAddress sources (query vs body) correctly","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"status":"passed","title":"handles mixed userAddress sources (query vs body) correctly"},{"ancestorTitles":["IP-based Rate Limiting (Outer Layer)"],"duration":123,"failureDetails":[],"failureMessages":[],"fullName":"IP-based Rate Limiting (Outer Layer) still applies to all API endpoints","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"still applies to all API endpoints"}],"endTime":1774864274385,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/integration.test.ts","startTime":1774864273025,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["StellarService","getAccount"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"StellarService getAccount should fetch account information","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should fetch account information"},{"ancestorTitles":["StellarService","getAccount"],"duration":22,"failureDetails":[],"failureMessages":[],"fullName":"StellarService getAccount should throw error when account fetch fails","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should throw error when account fetch fails"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction should submit transaction successfully","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should submit transaction successfully"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction returns failure when provider reports unsuccessful on-chain execution (HTTP 200)","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"status":"passed","title":"returns failure when provider reports unsuccessful on-chain execution (HTTP 200)"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction should handle transaction submission failure","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle transaction submission failure"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction retries on transient 5xx errors with exponential backoff and then succeeds","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"retries on transient 5xx errors with exponential backoff and then succeeds"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction does not retry on 4xx client errors (e.g., 401)","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"does not retry on 4xx client errors (e.g., 401)"},{"ancestorTitles":["StellarService","submitTransaction"],"duration":6,"failureDetails":[],"failureMessages":[],"fullName":"StellarService submitTransaction stops after max retries on persistent 5xx errors and returns failure","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"stops after max retries on persistent 5xx errors and returns failure"},{"ancestorTitles":["StellarService","monitorTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService monitorTransaction should monitor transaction until success","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should monitor transaction until success"},{"ancestorTitles":["StellarService","monitorTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService monitorTransaction should handle failed transaction","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should handle failed transaction"},{"ancestorTitles":["StellarService","monitorTransaction"],"duration":102,"failureDetails":[],"failureMessages":[],"fullName":"StellarService monitorTransaction should support cancellation via AbortSignal","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"status":"passed","title":"should support cancellation via AbortSignal"},{"ancestorTitles":["StellarService","monitorTransaction"],"duration":501,"failureDetails":[],"failureMessages":[],"fullName":"StellarService monitorTransaction should timeout if transaction takes too long","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should timeout if transaction takes too long"},{"ancestorTitles":["StellarService","monitorTransaction"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"StellarService monitorTransaction should use exponential backoff for polling","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should use exponential backoff for polling"},{"ancestorTitles":["StellarService","healthCheck"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"StellarService healthCheck should return healthy status for all services","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return healthy status for all services"},{"ancestorTitles":["StellarService","healthCheck"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService healthCheck should return unhealthy status when services fail","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return unhealthy status when services fail"},{"ancestorTitles":["StellarService","getProtocolStats"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService getProtocolStats should fetch and normalize protocol stats from the contract report","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should fetch and normalize protocol stats from the contract report"},{"ancestorTitles":["StellarService","getProtocolStats"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService getProtocolStats should return cached protocol stats within the TTL window","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should return cached protocol stats within the TTL window"},{"ancestorTitles":["StellarService","buildUnsignedTransaction"],"duration":1,"failureDetails":[],"failureMessages":[],"fullName":"StellarService buildUnsignedTransaction should build unsigned deposit transaction without requiring a secret key","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should build unsigned deposit transaction without requiring a secret key"},{"ancestorTitles":["StellarService","buildUnsignedTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService buildUnsignedTransaction should build unsigned borrow transaction without requiring a secret key","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should build unsigned borrow transaction without requiring a secret key"},{"ancestorTitles":["StellarService","buildUnsignedTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService buildUnsignedTransaction should build unsigned repay transaction without requiring a secret key","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should build unsigned repay transaction without requiring a secret key"},{"ancestorTitles":["StellarService","buildUnsignedTransaction"],"duration":0,"failureDetails":[],"failureMessages":[],"fullName":"StellarService buildUnsignedTransaction should build unsigned withdraw transaction without requiring a secret key","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should build unsigned withdraw transaction without requiring a secret key"}],"endTime":1774864274388,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/stellar.service.test.ts","startTime":1774864273124,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":23,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject empty userAddress","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should reject empty userAddress"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject invalid Stellar public key","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should reject invalid Stellar public key"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject Stellar address with wrong prefix","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should reject Stellar address with wrong prefix"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":1904,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should accept valid Stellar public key","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should accept valid Stellar public key"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject missing amount","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject missing amount"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":5,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject zero amount","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should reject zero amount"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":7,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject negative amount","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"status":"passed","title":"should reject negative amount"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject non-integer amount strings","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject non-integer amount strings"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":7,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject non-numeric amount strings","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject non-numeric amount strings"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject empty amount strings","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject empty amount strings"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should accept very large valid integers (within i128)","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should accept very large valid integers (within i128)"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should accept MAX_SAFE_INTEGER","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should accept MAX_SAFE_INTEGER"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should accept very large numbers","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should accept very large numbers"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject float amounts","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject float amounts"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject scientific notation","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject scientific notation"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":3,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject negative zero","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"status":"passed","title":"should reject negative zero"},{"ancestorTitles":["Validation Middleware","Prepare Validation (GET /api/lending/prepare/:operation)"],"duration":4,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Prepare Validation (GET /api/lending/prepare/:operation) should reject invalid operation","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should reject invalid operation"},{"ancestorTitles":["Validation Middleware","Submit Validation (POST /api/lending/submit)"],"duration":10,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Submit Validation (POST /api/lending/submit) should reject missing signedXdr","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should reject missing signedXdr"},{"ancestorTitles":["Validation Middleware","Submit Validation (POST /api/lending/submit)"],"duration":2,"failureDetails":[],"failureMessages":[],"fullName":"Validation Middleware Submit Validation (POST /api/lending/submit) should reject empty signedXdr","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"status":"passed","title":"should reject empty signedXdr"}],"endTime":1774864276045,"message":"","name":"/Users/bashir/Documents/stellarlend/api/src/__tests__/validation.test.ts","startTime":1774864273056,"status":"passed","summary":""}],"wasInterrupted":false}