Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"kiroAgent.configureMCP": "Disabled"
}
}
28 changes: 28 additions & 0 deletions docs/backend/data-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Data Flow

The standard request lifecycle (e.g., fetching or updating a contract) follows this step-by-step flow:

1. **Client Request:**
A request hits an endpoint (e.g., `GET /api/v1/contracts`).

2. **Middleware (Validation/Auth):**
- Global middleware parses JSON (`express.json()`) and adds security headers (`helmet()`).
- Route-specific middleware (`validateSchema`) validates the request payload structure.

3. **Controller (`ContractsController`):**
- Receives the validated request.
- Extracts parameters (e.g., contract ID).
- Calls the corresponding Service method.

4. **Service/Business Logic (`ContractsService`):**
- Executes business rules.
- May call the Database (for metadata) and the `SorobanService` (to fetch on-chain state).
- Aggregates the results.

5. **Response:**
- The Controller receives the aggregated data from the Service.
- Formats the HTTP response and sends it back to the client.

## Integration Points
- **Stellar/Soroban Network:** Interaction via Stellar SDK/Soroban RPC. Reads smart contract states and events.
- **Authentication Provider:** Signature verification to authenticate users based on their Stellar keypairs.
21 changes: 21 additions & 0 deletions docs/backend/modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Module Breakdown

Based on the application's domain (freelancer escrow, Soroban integration), the backend is organized into the following core modules:

## Contracts Module (`src/modules/contracts/`)
- **Responsibilities:**
- Manage off-chain metadata for escrow contracts.
- Synchronize state with Soroban smart contracts.
- **Dependencies:**
- Depends on `SorobanService` for on-chain state queries.

## Reputation Module (`src/modules/reputation/`)
- **Responsibilities:**
- Track and calculate user/freelancer reputation scores based on completed contracts and reviews.
- **Dependencies:**
- Relies on Contract data to verify completed work.

## Soroban Integration Module (`src/services/soroban.service.ts`)
- **Responsibilities:**
- Bridge the backend with the Stellar network.
- Read contract states, listen for events, and optionally submit transactions securely.
46 changes: 46 additions & 0 deletions err2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FAIL src/controllers/contracts.controller.test.ts
ContractsController fallback errors
× should catch error in getContracts and call next() (5 ms)
× should catch error in createContract and call next() (1 ms)

● ContractsController fallback errors › should catch error in getContracts and call next()

expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: [Error: DB Down]

Number of calls: 0

  33 | await ContractsController.getContracts(mockRequest as Request, mockResponse as Response, mockNext);
 34 |
> 35 | expect(mockNext).toHaveBeenCalledWith(mockError);
 | ^
 36 | });
 37 |
 38 | it('should catch error in createContract and call next()', async () => {

at Object.<anonymous> (src/controllers/contracts.controller.test.ts:35:22)

● ContractsController fallback errors › should catch error in createContract and call next()

expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: [Error: Creation failed]

Number of calls: 0

  42 | await ContractsController.createContract(mockRequest as Request, mockResponse as Response, mockNext);
 43 |
> 44 | expect(mockNext).toHaveBeenCalledWith(mockError);
 | ^
 45 | });
 46 | });
 47 |

at Object.<anonymous> (src/controllers/contracts.controller.test.ts:44:22)

Test Suites: 1 failed, 1 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: 2.312 s, estimated 3 s
Ran all test suites matching /src\\controllers\\contracts.controller.test.ts/i.
Empty file added out1.txt
Empty file.
Empty file added out2.txt
Empty file.
Binary file added results.json
Binary file not shown.
58 changes: 58 additions & 0 deletions src/controllers/contracts.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Request, Response, NextFunction } from 'express';

const mockGetAllContracts = jest.fn();
const mockCreateContract = jest.fn();

jest.mock('../services/contracts.service', () => {
return {
ContractsService: jest.fn().mockImplementation(() => {
return {
getAllContracts: mockGetAllContracts,
createContract: mockCreateContract,
};
}),
};
});

import { ContractsController } from './contracts.controller';

describe('ContractsController fallback errors', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: NextFunction;

beforeEach(() => {
mockRequest = {
body: { title: 'Test Contract' }
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
mockNext = jest.fn();
mockGetAllContracts.mockClear();
mockCreateContract.mockClear();
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should catch error in getContracts and call next()', async () => {
const mockError = new Error('DB Down');
mockGetAllContracts.mockRejectedValue(mockError);

await ContractsController.getContracts(mockRequest as Request, mockResponse as Response, mockNext);

expect(mockNext).toHaveBeenCalledWith(mockError);
});

it('should catch error in createContract and call next()', async () => {
const mockError = new Error('Creation failed');
mockCreateContract.mockRejectedValue(mockError);

await ContractsController.createContract(mockRequest as Request, mockResponse as Response, mockNext);

expect(mockNext).toHaveBeenCalledWith(mockError);
});
});
40 changes: 40 additions & 0 deletions src/controllers/contracts.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from 'express';
import { ContractsService } from '../services/contracts.service';
import { CreateContractDto } from '../modules/contracts/dto/contract.dto';

const contractsService = new ContractsService();

/**
* @dev Presentation layer for Contracts.
* Handles HTTP requests, extracts parameters, and formulates responses.
* Delegates core logic to the ContractsService.
*/
export class ContractsController {

/**
* GET /api/v1/contracts
* Fetch a list of all escrow contracts.
*/
public static async getContracts(req: Request, res: Response, next: NextFunction) {
try {
const contracts = await contractsService.getAllContracts();
res.status(200).json({ status: 'success', data: contracts });
} catch (error) {
next(error);
}
}

/**
* POST /api/v1/contracts
* Create a new escrow contract metadata entry.
*/
public static async createContract(req: Request, res: Response, next: NextFunction) {
try {
const data: CreateContractDto = req.body;
const newContract = await contractsService.createContract(data);
res.status(201).json({ status: 'success', data: newContract });
} catch (error) {
next(error);
}
}
}
69 changes: 69 additions & 0 deletions src/controllers/contracts.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import request from 'supertest';
import app from '../index';

describe('Contracts API Integration Tests', () => {
describe('GET /health', () => {
it('should return health status', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
expect(res.body).toEqual({ status: 'ok', service: 'talenttrust-backend' });
});
});

describe('GET /api/v1/contracts', () => {
it('should return a list of contracts (initially empty)', async () => {
const res = await request(app).get('/api/v1/contracts');
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ status: 'success', data: [] });
});
});

describe('POST /api/v1/contracts', () => {
it('should create a new contract with valid input', async () => {
const payload = {
title: 'Valid Contract Title',
description: 'This is a valid long enough description.',
budget: 1000
};

const res = await request(app)
.post('/api/v1/contracts')
.send(payload);

expect(res.status).toBe(201);
expect(res.body.status).toBe('success');
expect(res.body.data.title).toBe(payload.title);
expect(res.body.data.id).toBeDefined();
});

it('should return 400 validation error with invalid input (missing title)', async () => {
const payload = {
description: 'This is a valid long enough description.',
budget: 1000
};

const res = await request(app)
.post('/api/v1/contracts')
.send(payload);

expect(res.status).toBe(400);
expect(res.body.status).toBe('error');
expect(res.body.message).toBe('Validation failed');
});

it('should return 400 validation error with invalid budget (negative)', async () => {
const payload = {
title: 'Valid Contract Title',
description: 'This is a valid long enough description.',
budget: -50
};

const res = await request(app)
.post('/api/v1/contracts')
.send(payload);

expect(res.status).toBe(400);
expect(res.body.status).toBe('error');
});
});
});
65 changes: 65 additions & 0 deletions src/middleware/error.middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Request, Response, NextFunction } from 'express';
import { errorHandler } from './error.middleware';

describe('Error Middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: NextFunction;

beforeEach(() => {
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
mockNext = jest.fn();
// Suppress console.error in tests
jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should handle standard 500 error', () => {
const err = new Error('Test error');
errorHandler(err, mockRequest as Request, mockResponse as Response, mockNext);

expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
status: 'error',
statusCode: 500,
message: 'Test error',
});
});

it('should handle custom error status', () => {
const err: any = new Error('Not found');
err.status = 404;
errorHandler(err, mockRequest as Request, mockResponse as Response, mockNext);

expect(mockResponse.status).toHaveBeenCalledWith(404);
expect(mockResponse.json).toHaveBeenCalledWith({
status: 'error',
statusCode: 404,
message: 'Not found',
});
});

it('should obscure 500 errors in production', () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';

const err = new Error('Sensitive DB Error');
errorHandler(err, mockRequest as Request, mockResponse as Response, mockNext);

expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
status: 'error',
statusCode: 500,
message: 'Internal server error',
});

process.env.NODE_ENV = originalEnv;
});
});
26 changes: 26 additions & 0 deletions src/middleware/error.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, Response, NextFunction } from 'express';

/**
* @dev Global error handling middleware.
* Ensures that unexpected errors do not leak stack traces or internal
* logic to the client, especially in production environments.
*
* @param err The error object.
* @param req The Express Request.
* @param res The Express Response.
* @param next The Express NextFunction.
*/
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
const statusCode = err.status || 500;
const message = err.message || 'Internal Server Error';

console.error(`[Error] ${statusCode} - ${message}`, err.stack);

res.status(statusCode).json({
status: 'error',
statusCode,
message: process.env.NODE_ENV === 'production' && statusCode === 500
? 'Internal server error'
: message,
});
};
Loading
Loading