Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4a286d3
feat(api): sanitize and normalize user-supplied request fields
Properprogress1 Mar 26, 2026
e8c56bc
chore: sync package-lock.json with package.json
Properprogress1 Mar 27, 2026
2a3113d
chore(ci): fix lint errors and jsdoc compliance
Properprogress1 Mar 27, 2026
816730b
Merge branch 'main' into feature/input-sanitization-pipeline
Properprogress1 Mar 28, 2026
368930c
fix(ci): run npm commands from app subdirectory
Properprogress1 Mar 28, 2026
858797a
fix(ci): avoid lockfile path/cache resolution failures
Properprogress1 Mar 28, 2026
874c0f1
fix(lint): resolve CI parsing, jsdoc, and no-undef errors
Properprogress1 Mar 28, 2026
9422dfe
fix(app): export express instance for supertest compatibility
Properprogress1 Mar 28, 2026
4e58de7
fix(test-compat): add missing routes, exports, and envelope behavior
Properprogress1 Mar 29, 2026
e71a448
fix(error-handler): return standardized error envelope
Properprogress1 Mar 29, 2026
1bd1255
fix(errors): unify envelope format, headers, and test expectations
Properprogress1 Mar 29, 2026
52cde68
fix(ci): align error/auth behavior, soroban retry, and coverage thres…
Properprogress1 Mar 29, 2026
2f4a9b7
test(auth): align escrow auth test with GET /api/escrow/:invoiceId
Properprogress1 Mar 29, 2026
578cc61
fix(ci-tests): remove vitest/knex blockers and restore invoice+soroba…
Properprogress1 Mar 29, 2026
e3a9863
fix body-size limits middleware and align cors expectations
Properprogress1 Mar 29, 2026
97d67f5
Merge branch 'main' into feature/input-sanitization-pipeline
mikewheeleer Apr 1, 2026
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cache-dependency-path: "package-lock.json"

- name: Install dependencies
run: npm ci
run: npm install --package-lock=false

- name: Lint
run: npm run lint
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ module.exports = [
it: 'readonly',
expect: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
beforeEach: 'readonly',
afterAll: 'readonly',
afterEach: 'readonly',
},
},
Expand Down
166 changes: 83 additions & 83 deletions src/__tests__/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,83 @@
const request = require('supertest');
const jwt = require('jsonwebtoken');
const app = require('../index'); // Adjust if needed based on index.js exports

describe('Authentication Middleware', () => {
const secret = process.env.JWT_SECRET || 'test-secret';
const validPayload = { id: 1, role: 'user' };
let validToken;
let expiredToken;

beforeAll(() => {
validToken = jwt.sign(validPayload, secret, { expiresIn: '1h' });
expiredToken = jwt.sign(validPayload, secret, { expiresIn: '-1h' });
});

describe('Route Protection - POST /api/invoices', () => {
it('should return 401 when no token is provided', async () => {
const response = await request(app).post('/api/invoices').send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication token is required');
});

it('should return 401 when token format is invalid (missing Bearer)', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `FakeBearer ${validToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid Authorization header format. Expected "Bearer <token>"');
});

it('should return 401 when authorization header is malformed (no space)', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer${validToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid Authorization header format. Expected "Bearer <token>"');
});

it('should return 401 when token is invalid', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', 'Bearer some.invalid.token')
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid token');
});

it('should return 401 when token is expired', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${expiredToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Token has expired');
});

it('should return 201 when a valid token is provided', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send({ amount: 1000, customer: 'Test Corp' });
expect(response.status).toBe(201);
expect(response.body.data).toHaveProperty('id');
});
});

describe('Route Protection - GET /api/escrow/:invoiceId', () => {
it('should allow escrow read with valid token', async () => {
const response = await request(app)
.get('/api/escrow/test-invoice')
.set('Authorization', `Bearer ${validToken}`);
expect(response.status).toBe(200);
expect(response.body.data.invoiceId).toBe('test-invoice');
});

it('should reject escrow read without token', async () => {
const response = await request(app).get('/api/escrow/test-invoice');
expect(response.status).toBe(401);
});
});
});
const request = require('supertest');
const jwt = require('jsonwebtoken');
const app = require('../index'); // Adjust if needed based on index.js exports
describe('Authentication Middleware', () => {
const secret = process.env.JWT_SECRET || 'test-secret';
const validPayload = { id: 1, role: 'user' };
let validToken;
let expiredToken;
beforeAll(() => {
validToken = jwt.sign(validPayload, secret, { expiresIn: '1h' });
expiredToken = jwt.sign(validPayload, secret, { expiresIn: '-1h' });
});
describe('Route Protection - POST /api/invoices', () => {
it('should return 401 when no token is provided', async () => {
const response = await request(app).post('/api/invoices').send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication token is required');
});
it('should return 401 when token format is invalid (missing Bearer)', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `FakeBearer ${validToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid Authorization header format. Expected "Bearer <token>"');
});
it('should return 401 when authorization header is malformed (no space)', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer${validToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid Authorization header format. Expected "Bearer <token>"');
});
it('should return 401 when token is invalid', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', 'Bearer some.invalid.token')
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid token');
});
it('should return 401 when token is expired', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${expiredToken}`)
.send({});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Token has expired');
});
it('should return 201 when a valid token is provided', async () => {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send({ amount: 1000, customer: 'Test Corp' });
expect(response.status).toBe(201);
expect(response.body.data).toHaveProperty('id');
});
});
describe('Route Protection - GET /api/escrow/:invoiceId', () => {
it('should allow escrow read with valid token', async () => {
const response = await request(app)
.get('/api/escrow/test-invoice')
.set('Authorization', `Bearer ${validToken}`);
expect(response.status).toBe(200);
expect(response.body.data.invoiceId).toBe('test-invoice');
});
it('should reject escrow read without token', async () => {
const response = await request(app).get('/api/escrow/test-invoice');
expect(response.status).toBe(401);
});
});
});
5 changes: 2 additions & 3 deletions src/__tests__/bodySizeLimits.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@

describe('DEFAULT_LIMITS', () => {
it.each(['json', 'urlencoded', 'raw', 'invoice'])('%s is a parseable string', (key) => {
expect(typeof DEFAULT_LIMITS[key]).toBe('string');

Check warning on line 129 in src/__tests__/bodySizeLimits.test.js

View workflow job for this annotation

GitHub Actions / build-and-lint

Generic Object Injection Sink
expect(parseSize(DEFAULT_LIMITS[key])).toBeGreaterThan(0);

Check warning on line 130 in src/__tests__/bodySizeLimits.test.js

View workflow job for this annotation

GitHub Actions / build-and-lint

Function Call Object Injection Sink
});
});

Expand Down Expand Up @@ -441,8 +441,7 @@
});
it('increases with attempt number', () => {
const d3 = computeBackoff(3, 200, 5000);
// With jitter d3 is almost certainly larger; we check average tendency
expect(200 * 2 ** 3).toBeGreaterThan(200); // sanity
expect(d3).toBeGreaterThanOrEqual(d0);

Check failure on line 444 in src/__tests__/bodySizeLimits.test.js

View workflow job for this annotation

GitHub Actions / build-and-lint

'd0' is not defined
expect(d3).toBeLessThanOrEqual(5000);
});
it('is capped at maxDelay', () => {
Expand Down Expand Up @@ -689,4 +688,4 @@
// GET is not affected; the 100 KB global limit only applies to bodies.
expect(res.status).toBe(200);
});
});
});
8 changes: 5 additions & 3 deletions src/__tests__/invoice.api.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const request = require('supertest');
const { createApp } = require('../app');
const invoiceService = require('../services/invoice.service');

// Mock the service
jest.mock('../services/invoice.service');
jest.mock('../services/invoice.service', () => ({
getInvoices: jest.fn(),
}));

const invoiceService = require('../services/invoice.service');

describe('Invoice API Integration', () => {
let app;
Expand Down
72 changes: 36 additions & 36 deletions src/__tests__/rateLimit.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
const request = require('supertest');
const jwt = require('jsonwebtoken');
const app = require('../index');

describe('Rate Limiting Middleware', () => {
const secret = process.env.JWT_SECRET || 'test-secret';
const validToken = jwt.sign({ id: 'test_user_1' }, secret);
const validBody = { amount: 100, customer: 'Rate Test Corp' };

it('should return 200 for health check (global limiter allows many)', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.headers).toHaveProperty('ratelimit-limit');
});

describe('Sensitive Operations Throttling - POST /api/invoices', () => {
it('should allow up to 10 requests and then return 429 Too Many Requests', async () => {
for (let i = 0; i < 10; i++) {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send(validBody);
if (response.status === 429) {
break;
}
expect(response.status).toBe(201);
}
const throttledResponse = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send(validBody);
expect(throttledResponse.status).toBe(429);
expect(throttledResponse.body.error).toContain('rate limit exceeded');
});
});
});
const request = require('supertest');
const jwt = require('jsonwebtoken');
const app = require('../index');
describe('Rate Limiting Middleware', () => {
const secret = process.env.JWT_SECRET || 'test-secret';
const validToken = jwt.sign({ id: 'test_user_1' }, secret);
const validBody = { amount: 100, customer: 'Rate Test Corp' };
it('should return 200 for health check (global limiter allows many)', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.headers).toHaveProperty('ratelimit-limit');
});
describe('Sensitive Operations Throttling - POST /api/invoices', () => {
it('should allow up to 10 requests and then return 429 Too Many Requests', async () => {
for (let i = 0; i < 10; i++) {
const response = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send(validBody);
if (response.status === 429) {
break;
}
expect(response.status).toBe(201);
}
const throttledResponse = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${validToken}`)
.send(validBody);
expect(throttledResponse.status).toBe(429);
expect(throttledResponse.body.error).toContain('rate limit exceeded');
});
});
});
Loading
Loading