diff --git a/src/config/configuration.spec.ts b/src/config/configuration.spec.ts new file mode 100644 index 00000000..d5e459fd --- /dev/null +++ b/src/config/configuration.spec.ts @@ -0,0 +1,58 @@ +import configuration from './configuration'; + +describe('configuration (#601)', () => { + const ORIG = process.env; + beforeEach(() => { + process.env = { ...ORIG }; + }); + afterAll(() => { + process.env = ORIG; + }); + + it('uses dev defaults when env is unset', () => { + delete process.env.NODE_ENV; + delete process.env.PORT; + const cfg = configuration(); + expect(cfg.env).toBe('development'); + expect(cfg.jwt.secret).toBe('insecure-dev-secret'); + expect(cfg.stellar.network).toBe('testnet'); + }); + + it('parses integer env vars', () => { + process.env.PORT = '9000'; + process.env.ESCROW_DEADLINE_SECONDS = '3600'; + const cfg = configuration(); + expect(cfg.port).toBe(9000); + expect(cfg.stellar.escrowDeadlineSeconds).toBe(3600); + }); + + it('parses boolean env vars strictly (=== "true")', () => { + process.env.DATABASE_SYNCHRONIZE = 'true'; + process.env.DATABASE_LOGGING = 'false'; + const cfg = configuration(); + expect(cfg.database.synchronize).toBe(true); + expect(cfg.database.logging).toBe(false); + }); + + it('falls back MAINTENANCE_POOL_CONTRACT_ID to ESCROW_CONTRACT_ID', () => { + delete process.env.MAINTENANCE_POOL_CONTRACT_ID; + process.env.ESCROW_CONTRACT_ID = 'escrow-abc'; + expect(configuration().stellar.maintenancePoolContractId).toBe( + 'escrow-abc', + ); + }); + + it('prefers MAINTENANCE_POOL_CONTRACT_ID over the escrow fallback', () => { + process.env.MAINTENANCE_POOL_CONTRACT_ID = 'pool-xyz'; + process.env.ESCROW_CONTRACT_ID = 'escrow-abc'; + expect(configuration().stellar.maintenancePoolContractId).toBe('pool-xyz'); + }); + + it('reads the per-asset contract ids', () => { + process.env.USDC_TOKEN_CONTRACT_ID = 'usdc-1'; + process.env.XLM_TOKEN_CONTRACT_ID = 'xlm-1'; + const cfg = configuration(); + expect(cfg.stellar.assetContractIds.USDC).toBe('usdc-1'); + expect(cfg.stellar.assetContractIds.XLM).toBe('xlm-1'); + }); +}); diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 40e91db2..b7fca6bd 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -67,9 +67,32 @@ export interface AppConfig { }; } +/** + * Parse a numeric env var, failing loudly at config-load if it isn't a + * positive integer. A bare `parseInt` returns NaN on garbage input — for + * ESCROW_DEADLINE_SECONDS that NaN reached `resolveDeadline` and threw + * `RangeError: cannot convert NaN to BigInt` on every escrow fund, after the + * PENDING row was already saved (#610). Validate here so a bad value is a + * boot-time config error, not a mid-transaction crash. + */ +function envInt( + name: string, + raw: string | undefined, + fallback: number, +): number { + const v = + raw === undefined || raw === '' ? fallback : Number.parseInt(raw, 10); + if (!Number.isFinite(v) || v <= 0) { + throw new Error( + `Invalid ${name}: expected a positive integer, got '${raw}'`, + ); + } + return v; +} + export default (): AppConfig => ({ env: process.env.NODE_ENV ?? 'development', - port: parseInt(process.env.PORT ?? '3000', 10), + port: envInt('PORT', process.env.PORT, 3000), appUrl: process.env.APP_URL ?? 'http://localhost:3000', frontendUrl: process.env.FRONTEND_URL ?? 'http://localhost:3001', logLevel: process.env.LOG_LEVEL ?? 'debug', @@ -96,9 +119,10 @@ export default (): AppConfig => ({ }, analytics: { - platformSummaryTtlMs: parseInt( - process.env.ANALYTICS_PLATFORM_SUMMARY_TTL_MS ?? '60000', - 10, + platformSummaryTtlMs: envInt( + 'ANALYTICS_PLATFORM_SUMMARY_TTL_MS', + process.env.ANALYTICS_PLATFORM_SUMMARY_TTL_MS, + 60000, ), }, stellar: { @@ -118,9 +142,10 @@ export default (): AppConfig => ({ USDC: process.env.USDC_TOKEN_CONTRACT_ID ?? '', XLM: process.env.XLM_TOKEN_CONTRACT_ID ?? '', }, - escrowDeadlineSeconds: parseInt( - process.env.ESCROW_DEADLINE_SECONDS ?? '7776000', - 10, + escrowDeadlineSeconds: envInt( + 'ESCROW_DEADLINE_SECONDS', + process.env.ESCROW_DEADLINE_SECONDS, + 7776000, ), }, }); diff --git a/src/escrow/escrow.service.ts b/src/escrow/escrow.service.ts index 6d2dd289..c252993f 100644 --- a/src/escrow/escrow.service.ts +++ b/src/escrow/escrow.service.ts @@ -584,20 +584,14 @@ export class EscrowService { recipients: Array<[string, number]>, manager?: EntityManager, ): Promise { - return this.invokeOnLockedEscrow(escrow, operation, () => - this.soroban.invoke( - 'release', - // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). - [u64(this.onChainKeyFor(escrow)), recipients], - this.contractOpts(escrow), - ), return this.invokeOnLockedEscrow( escrow, operation, () => this.soroban.invoke( 'release', - [this.onChainKeyFor(escrow), recipients], + // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). + [u64(this.onChainKeyFor(escrow)), recipients], this.contractOpts(escrow), ), manager, diff --git a/src/github/github-webhooks.service.spec.ts b/src/github/github-webhooks.service.spec.ts index 45bb6fe8..a35ac3be 100644 --- a/src/github/github-webhooks.service.spec.ts +++ b/src/github/github-webhooks.service.spec.ts @@ -869,6 +869,9 @@ describe('GithubWebhooksService', () => { expect(bountiesService.markInReview).not.toHaveBeenCalled(); expect(bountiesService.markMergedAndRelease).not.toHaveBeenCalled(); + }); + }); + // #308: a redelivered X-GitHub-Delivery used to hit the unique constraint // on webhook_events.deliveryId and escape handleEvent as a 500, so every // redelivery of that event failed forever. diff --git a/src/sponsors/sponsors.controller.ts b/src/sponsors/sponsors.controller.ts index b7a48cca..c097bf1e 100644 --- a/src/sponsors/sponsors.controller.ts +++ b/src/sponsors/sponsors.controller.ts @@ -26,25 +26,6 @@ function validatePage(limit?: number, offset?: number) { throw new BadRequestException('offset must be >= 0'); } return { limit, offset }; -/** - * Parse optional limit/offset query params, rejecting NaN, negatives, - * non-integers, and limits above MAX_PAGE_LIMIT with a 400 (#280). - */ -function parsePagination(limit?: string, offset?: string) { - const parse = (name: string, raw: string | undefined, min: number, max: number) => { - if (raw === undefined || raw === '') return undefined; - const n = Number(raw); - if (!Number.isInteger(n) || n < min || n > max) { - throw new BadRequestException( - `${name} must be an integer between ${min} and ${max}`, - ); - } - return n; - }; - return { - limit: parse('limit', limit, 1, MAX_PAGE_LIMIT), - offset: parse('offset', offset, 0, Number.MAX_SAFE_INTEGER), - }; } @ApiTags('sponsors') @@ -73,7 +54,6 @@ export class SponsorsController { ) { this.assertOwnsSponsor(user, id); return this.sponsorsService.dashboard(id, validatePage(limit, offset)); - return this.sponsorsService.dashboard(id, parsePagination(limit, offset)); } @ApiBearerAuth() @@ -91,6 +71,5 @@ export class SponsorsController { id, validatePage(limit, offset), ); - return this.sponsorsService.milestoneProgress(id, parsePagination(limit, offset)); } }