From 97f6401b789ba8a17ca15a7a44d29f4b2a813116 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:34:03 +0000 Subject: [PATCH] Enable rate limiting, fix fastify-jwt dependency, and add regression test --- .jules/sentinel.md | 4 ++ backend/jest.config.js | 9 +++ backend/jest.setup.js | 43 +++++++++++++ backend/package-lock.json | 79 +++++------------------- backend/package.json | 2 +- backend/src/__tests__/rate_limit.test.ts | 70 +++++++++++++++++++++ backend/src/app.ts | 23 +++---- 7 files changed, 153 insertions(+), 77 deletions(-) create mode 100644 .jules/sentinel.md create mode 100644 backend/jest.config.js create mode 100644 backend/jest.setup.js create mode 100644 backend/src/__tests__/rate_limit.test.ts diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..a29ea45 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-01-16 - Dependency Version Mismatch in Fastify Ecosystem +**Vulnerability:** Application availability risk (DoS) due to dependency incompatibility. +**Learning:** `package.json` contained `@fastify/jwt` v10 which requires Fastify v5, but Fastify v4 was installed. This caused the application to crash on startup (`FST_ERR_PLUGIN_VERSION_MISMATCH`). The build process (`tsc`) did not catch this as it only checks types. +**Prevention:** Pin dependencies strictly when working with Fastify plugins, or use `npm audit` and runtime checks in CI to detect startup failures. When upgrading Fastify plugins, always check the required Fastify version. diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000..0bc0590 --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + setupFilesAfterEnv: ['/jest.setup.js'], + testMatch: ['**/__tests__/**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; diff --git a/backend/jest.setup.js b/backend/jest.setup.js new file mode 100644 index 0000000..d7a12d3 --- /dev/null +++ b/backend/jest.setup.js @@ -0,0 +1,43 @@ +// Set critical env vars for testing +process.env.NODE_ENV = 'test'; +process.env.JWT_SECRET = 'test-secret-at-least-32-chars-long-for-security'; +process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/launchpad_test'; +process.env.LUNES_RPC_URL = 'http://localhost:9999'; // Mock +process.env.CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000000'; +process.env.PRIVATE_KEY = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +// Mock IORedis +jest.mock('ioredis', () => { + return jest.fn().mockImplementation(() => { + return { + on: jest.fn(), + publish: jest.fn(), + set: jest.fn(), + get: jest.fn(), + del: jest.fn(), + quit: jest.fn(), + disconnect: jest.fn(), + }; + }); +}); + +// Mock Logger +const mockLogger = { + info: console.log, + error: console.error, + warn: console.warn, + debug: console.debug, + http: console.log, + blockchain: console.log, + database: console.log, + api: console.log, + auth: console.log, + security: console.log, + performance: console.log, + audit: console.log, +}; + +jest.mock('./src/shared/logger', () => ({ + logger: mockLogger, + Logger: mockLogger, // Logger class with static methods mimics the object +})); diff --git a/backend/package-lock.json b/backend/package-lock.json index c5266d5..d5afb9d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "^8.0.0", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", @@ -1536,60 +1536,18 @@ } }, "node_modules/@fastify/jwt": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.0.0.tgz", - "integrity": "sha512-2Qka3NiyNNcsfejMUvyzot1T4UYIzzcbkFGDdVyrl344fRZ/WkD6VFXOoXhxe2Pzf3LpJNkoSxUM4Ru4DVgkYA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-8.0.0.tgz", + "integrity": "sha512-pJHjmZaokteZFMbsVVt7pbyJpbDogTnpl/aD7eR3vLOPgfktp4k4gUM6cd7RtjL/Ol1qDwL5mup+vdNlZI3K0Q==", "license": "MIT", "dependencies": { - "@fastify/error": "^4.2.0", - "@lukeed/ms": "^2.0.2", - "fast-jwt": "^6.0.2", - "fastify-plugin": "^5.0.1", + "@fastify/error": "^3.0.0", + "@lukeed/ms": "^2.0.0", + "fast-jwt": "^3.3.2", + "fastify-plugin": "^4.0.0", "steed": "^1.1.3" } }, - "node_modules/@fastify/jwt/node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/jwt/node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -6860,27 +6818,18 @@ "license": "MIT" }, "node_modules/fast-jwt": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.0.2.tgz", - "integrity": "sha512-dTF4bhYnuXhZYQUaxsHKqAyA5y/L/kQc4fUu0wQ0BSA0dMfcNrcv0aqR2YnVi4f7e1OnzDVU7sDsNdzl1O5EVA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-3.3.3.tgz", + "integrity": "sha512-oS3P8bRI24oPLJUePt2OgF64FBQib5TlgHLFQxYNoHYEEZe0gU3cKjJAVqpB5XKV/zjxmq4Hzbk3fgfW/wRz8Q==", "license": "Apache-2.0", "dependencies": { - "@lukeed/ms": "^2.0.2", + "@lukeed/ms": "^2.0.1", "asn1.js": "^5.4.1", "ecdsa-sig-formatter": "^1.0.11", - "mnemonist": "^0.40.0" + "mnemonist": "^0.39.5" }, "engines": { - "node": ">=20" - } - }, - "node_modules/fast-jwt/node_modules/mnemonist": { - "version": "0.40.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", - "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", - "license": "MIT", - "dependencies": { - "obliterator": "^2.0.4" + "node": ">=16 <22" } }, "node_modules/fast-levenshtein": { diff --git a/backend/package.json b/backend/package.json index 11cd17a..644d9a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,7 +38,7 @@ "dependencies": { "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^10.0.0", + "@fastify/jwt": "^8.0.0", "@fastify/multipart": "^8.0.0", "@fastify/rate-limit": "^9.0.1", "@fastify/swagger": "^8.12.0", diff --git a/backend/src/__tests__/rate_limit.test.ts b/backend/src/__tests__/rate_limit.test.ts new file mode 100644 index 0000000..3cbd19f --- /dev/null +++ b/backend/src/__tests__/rate_limit.test.ts @@ -0,0 +1,70 @@ +import 'reflect-metadata'; +import { FastifyInstance } from 'fastify'; + +// Mock envConfig before importing app +jest.mock('../config/env.config', () => { + const original = jest.requireActual('../config/env.config'); + return { + envConfig: { + ...original.envConfig, + RATE_LIMIT_MAX_REQUESTS: 2, // Low limit for testing + RATE_LIMIT_WINDOW_MS: 1000, + NODE_ENV: 'test', + CORS_ORIGIN: '*', + JWT_SECRET: 'test-secret', + JWT_EXPIRES_IN: '1h', + ENABLE_SWAGGER: false, + PORT: 3000, + HOST: 'localhost' + } + }; +}); + +import { app } from '../app'; + +describe('Rate Limiting', () => { + let server: FastifyInstance; + + beforeAll(async () => { + // Mock server.listen to prevent actual port binding + jest.spyOn(app.server, 'listen').mockImplementation(async () => { + return 'http://localhost:3000'; + }); + + // Initialize the app (register plugins/routes) + await app.start(); + server = app.server; + }); + + afterAll(async () => { + await app.stop(); + }); + + it('should enforce rate limits', async () => { + // Make request 1 - should pass + const response1 = await server.inject({ + method: 'GET', + url: '/health' + }); + expect(response1.statusCode).toBe(200); + + // Make request 2 - should pass + const response2 = await server.inject({ + method: 'GET', + url: '/health' + }); + expect(response2.statusCode).toBe(200); + + // Make request 3 - should fail with 429 + const response3 = await server.inject({ + method: 'GET', + url: '/health' + }); + expect(response3.statusCode).toBe(429); + + // Verify error message structure if possible + const body = JSON.parse(response3.body); + expect(body.error.code).toBe(429); + expect(body.error.message).toContain('Muitas tentativas'); + }); +}); diff --git a/backend/src/app.ts b/backend/src/app.ts index 22bd2dd..c1b7d89 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -64,17 +64,18 @@ class App { }, }); - // Rate Limiting - DESABILITADO PARA DESENVOLVIMENTO - // await this.server.register(rateLimit, { - // max: envConfig.RATE_LIMIT_MAX_REQUESTS, - // timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, - // errorResponseBuilder: (request, context) => ({ - // code: 429, - // error: 'Rate Limit Exceeded', - // message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, - // expiresIn: context.ttl, - // }), - // }); + // Rate Limiting + await this.server.register(rateLimit, { + max: envConfig.RATE_LIMIT_MAX_REQUESTS, + timeWindow: envConfig.RATE_LIMIT_WINDOW_MS, + errorResponseBuilder: (request, context) => ({ + statusCode: 429, + code: 429, + error: 'Rate Limit Exceeded', + message: `Muitas tentativas. Tente novamente em ${Math.round(context.ttl / 1000)} segundos.`, + expiresIn: context.ttl, + }), + }); // Swagger Documentation if (envConfig.ENABLE_SWAGGER) {