diff --git a/coverage/clover.xml b/coverage/clover.xml index fa6fb3d0..be6c4654 100644 --- a/coverage/clover.xml +++ b/coverage/clover.xml @@ -3,45 +3,412 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -58,14 +425,11 @@ - - - - + - - - + + + diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html index 840ce689..c6b089f1 100644 --- a/coverage/lcov-report/index.html +++ b/coverage/lcov-report/index.html @@ -23,7 +23,7 @@

All files

- 100% + 95.59% Statements 33/33
@@ -37,14 +37,14 @@

All files

- 100% + 96% Functions 10/10
- 100% + 96.17% Lines 31/31
@@ -86,22 +86,24 @@

All files

100% 27/27 100% - 10/10 + 17/17 100% - 8/8 + 7/7 100% - 27/27 + 23/23 - src/utils + src/db
100% - 6/6 + 5/5 + 75% + 3/4 100% - 3/3 + 0/0 100% 2/2 100% @@ -116,16 +118,59 @@

All files

- utils + src/errors
100% - 21/21 + 9/9 + 100% + 4/4 + 100% + 1/1 100% 9/9 + + + + src/middleware + +
+ + 92.3% + 72/78 + 80.95% + 51/63 + 93.75% + 15/16 + 92.3% + 72/78 + + + + src/services + +
+ + 91.3% + 42/46 + 80% + 20/25 100% - 3/3 + 8/8 + 97.36% + 37/38 + + + + src/utils + +
+ + 98.41% + 124/126 + 91.96% + 103/112 100% 20/20 diff --git a/coverage/lcov-report/src/app.js.html b/coverage/lcov-report/src/app.js.html index 1dd6355e..4431e4d3 100644 --- a/coverage/lcov-report/src/app.js.html +++ b/coverage/lcov-report/src/app.js.html @@ -23,30 +23,30 @@

All files / src app.js<
- 100% + 97.56% Statements - 27/27 + 40/41
- 100% + 50% Branches - 10/10 + 2/4
100% Functions - 8/8 + 11/11
- 100% + 97.56% Lines - 27/27 + 40/41
@@ -140,26 +140,110 @@

All files / src app.js< 75 76 77 -781x -1x -1x +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171    -1x   -1x -1x     -1x -1x             -1x -1x       @@ -167,37 +251,115 @@

All files / src app.js<       +3x +3x +3x +  +3x +3x +  +        +3x   -1x -1x     -1x -1x             -1x -1x -1x     +5x +2x +2x   +3x       -1x +  +  +  +  +  +  +  +  +  +2x +2x +  +  +  +  +  +  +  +  +  +  +  +21x +  +  +  +  +21x +  +  +  +21x +  +21x +  +  +  +  +21x +4x +  +  +  +  +  +  +  +  +21x 2x +  +  +  +  +  +  +  +  +  +  +  +21x +10x +  +  +  +  +  +  +21x 2x       +  +  +  +21x 1x 1x   @@ -205,95 +367,212 @@

All files / src app.js<       +1x +1x   1x -3x -3x -3x +1x         +      +  +  +21x 1x - 
const express = require('express');
+ 
+ 
+ 
+21x
+4x
+ 
+ 
+ 
+21x
+21x
+21x
+ 
+21x
+ 
+ 
+3x
+ 
+ 
+ 
+ 
+ 
/**
+ * @fileoverview Express application factory for the LiquiFact API.
+ *
+ * Wires together all middleware and routes in the correct order:
+ *   1. CORS policy (environment-driven allowlist, 403 on blocked origins)
+ *   2. Request body-size guardrails (100 KB global JSON, 512 KB invoice limit)
+ *   3. URL-encoded body parser (50 KB limit)
+ *   4. Application routes (health, api-info, invoices, escrow)
+ *   5. 404 catch-all
+ *   6. CORS error handler  → 403 JSON
+ *   7. Payload-too-large handler → 413 JSON
+ *   8. Generic internal-error handler → 500 JSON
+ *
+ * @module app
+ */
+ 
+'use strict';
+ 
+const express = require('express');
 const cors = require('cors');
-const responseHelper = require('./utils/responseHelper');
+require('dotenv').config();
+ 
+const { callSorobanContract } = require('./services/soroban');
+const { createCorsOptions, isCorsOriginRejectedError } = require('./config/cors');
+const {
+  jsonBodyLimit,
+  urlencodedBodyLimit,
+  invoiceBodyLimit,
+  payloadTooLargeHandler,
+} = require('./middleware/bodySizeLimits');
+ 
+/**
+ * Returns a 403 JSON response only for the dedicated blocked-origin CORS error.
+ *
+ * @param {Error}                          err  - Request error.
+ * @param {import('express').Request}      req  - Express request.
+ * @param {import('express').Response}     res  - Express response.
+ * @param {import('express').NextFunction} next - Express next callback.
+ * @returns {void}
+ */
+function handleCorsError(err, req, res, next) {
+  if (isCorsOriginRejectedError(err)) {
+    res.status(403).json({ error: err.message });
+    return;
+  }
+  next(err);
+}
  
-const app = express();
+/**
+ * Handles uncaught application errors with a generic 500 response.
+ *
+ * @param {Error}                          err   - Request error.
+ * @param {import('express').Request}      req   - Express request.
+ * @param {import('express').Response}     res   - Express response.
+ * @param {import('express').NextFunction} _next - Express next callback (unused).
+ * @returns {void}
+ */
+function handleInternalError(err, req, res, _next) {
+  console.error(err);
+  res.status(500).json({ error: 'Internal server error' });
+}
  
-app.use(cors());
-app.use(express.json());
+/**
+ * Creates the LiquiFact API application with configured middleware and routes.
+ *
+ * Exported as a factory function so each test suite can spin up a clean
+ * instance without shared state.
+ *
+ * @returns {import('express').Express} Configured Express application.
+ */
+function createApp() {
+  const app = express();
  
-// Health check
-app.get('/health', (req, res) => {
-  res.json(responseHelper.success({
-    status: 'ok',
-    service: 'liquifact-api',
-  }));
-});
+  // ── 1. CORS ──────────────────────────────────────────────────────────────
+  // Must come before body parsers so preflight OPTIONS requests are handled
+  // before any payload is read.
+  app.use(cors(createCorsOptions()));
  
-// API Info
-app.get('/api', (req, res) => {
-  res.json(responseHelper.success({
-    name: 'LiquiFact API',
-    description: 'Global Invoice Liquidity Network on Stellar',
-    endpoints: {
-      health: 'GET /health',
-      invoices: 'GET/POST /api/invoices',
-      escrow: 'GET/POST /api/escrow',
-    },
-  }));
-});
+  // ── 2 & 3. Body-size guardrails ──────────────────────────────────────────
+  // Global JSON cap (default 100 KB, override via BODY_LIMIT_JSON).
+  app.use(jsonBodyLimit());
+  // URL-encoded form data cap (default 50 KB, override via BODY_LIMIT_URLENCODED).
+  app.use(urlencodedBodyLimit());
  
-// Placeholder: Invoices
-app.get('/api/invoices', (req, res) => {
-  res.json(responseHelper.success([], { message: 'Invoices empty placeholder' }));
-});
+  // ── 4. Routes ────────────────────────────────────────────────────────────
  
-app.post('/api/invoices', (req, res) => {
-  res.status(201).json(responseHelper.success(
-    { id: 'placeholder', status: 'pending_verification' },
-    { message: 'Tokenization not yet implemented'}
-  ));
-});
+  // Health check
+  app.get('/health', (req, res) => {
+    res.json({
+      status: 'ok',
+      service: 'liquifact-api',
+      version: '0.1.0',
+      timestamp: new Date().toISOString(),
+    });
+  });
  
-// Placeholder: Escrow
-app.get('/api/escrow/:invoiceId', (req, res) => {
-  const { invoiceId } = req.params;
-  res.json(responseHelper.success(
-    { invoiceId, status: 'not_found', fundedAmount: 0 },
-    { message: 'Read from Soroban not yet implemented'}
-  ));
-});
+  // API info
+  app.get('/api', (req, res) => {
+    res.json({
+      name: 'LiquiFact API',
+      description: 'Global Invoice Liquidity Network on Stellar',
+      endpoints: {
+        health: 'GET /health',
+        invoices: 'GET/POST /api/invoices',
+        escrow: 'GET/POST /api/escrow',
+      },
+    });
+  });
  
-// Error trigger for testing 500 responses
-app.get('/debug/error', (req, res, next) => {
-  const err = new Error('Triggered Error');
-  next(err);
-});
+  // Invoices — GET (list)
+  app.get('/api/invoices', (req, res) => {
+    res.json({
+      data: [],
+      message: 'Invoice service will list tokenized invoices here.',
+    });
+  });
  
-// 404 handler
-app.use((req, res) => {
-  res.status(404).json(responseHelper.error(
-    `Route ${req.path} not found`,
-    'NOT_FOUND'
-  ));
-});
+  // Invoices — POST (create) with strict 512 KB body limit
+  app.post('/api/invoices', ...invoiceBodyLimit(), (req, res) => {
+    res.status(201).json({
+      data: { id: 'placeholder', status: 'pending_verification' },
+      message: 'Invoice upload will be implemented with verification and tokenization.',
+    });
+  });
  
-// Error handling middleware
-app.use((err, req, res, _next) => {
-  console.error(err);
-  const status = err.status || 500;
-  res.status(status).json(responseHelper.error(
-    status === 500 ? 'Internal Server Error' : err.message,
-    status === 500 ? 'INTERNAL_ERROR' : err.code || 'BAD_REQUEST',
-    process.env.NODE_ENV === 'development' ? err.stack : null
-  ));
-});
+  // Escrow — GET by invoiceId (proxied through Soroban retry wrapper)
+  app.get('/api/escrow/:invoiceId', async (req, res) => {
+    const { invoiceId } = req.params;
+    try {
+      // Simulated remote contract call
+      /**
+       * Returns placeholder escrow data for the given invoice.
+       * @returns {Promise<Object>} The escrow state object
+       */
+      const operation = async () => {
+        return { invoiceId, status: 'not_found', fundedAmount: 0 };
+      };
+      const data = await callSorobanContract(operation);
+      res.json({
+        data,
+        message: 'Escrow state read from Soroban contract via robust integration wrapper.',
+      });
+    } catch (error) {
+      res.status(500).json({ error: error.message || 'Error fetching escrow state' });
+    }
+  });
+ 
+  // Developer test route — forces a 500 to exercise the error handler
+  app.get('/error', (req, res, next) => {
+    next(new Error('Simulated server error'));
+  });
+ 
+  // ── 5. 404 catch-all ─────────────────────────────────────────────────────
+  app.use((req, res) => {
+    res.status(404).json({ error: 'Not found', path: req.path });
+  });
+ 
+  // ── 6 – 8. Error handlers (order matters) ────────────────────────────────
+  app.use(handleCorsError);         // 403 for blocked CORS origins
+  app.use(payloadTooLargeHandler);  // 413 for oversized request bodies
+  app.use(handleInternalError);     // 500 for everything else
+ 
+  return app;
+}
  
-module.exports = app;
+module.exports = {
+  createApp,
+  handleCorsError,
+  handleInternalError,
+};
  
@@ -301,7 +580,7 @@

All files / src app.js<