From 237f1c9114c547a41d861f2f21b95911830cd391 Mon Sep 17 00:00:00 2001 From: Yggdrasil Date: Wed, 14 Oct 2026 01:25:01 -0400 Subject: [PATCH 1/2] test(config): spec the production-required-config validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate-required-config had no spec — the boot-guard that refuses to start in production with a missing/default secret. Covers: the no-op outside prod, the insecure-default JWT flag, the default DATABASE_URL, and the missing escrow/treasury secrets. Resolves #600 --- src/config/validate-required-config.spec.ts | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/config/validate-required-config.spec.ts diff --git a/src/config/validate-required-config.spec.ts b/src/config/validate-required-config.spec.ts new file mode 100644 index 00000000..9e95bd78 --- /dev/null +++ b/src/config/validate-required-config.spec.ts @@ -0,0 +1,89 @@ +import { + collectConfigIssues, + assertRequiredConfig, + RequiredConfig, + INSECURE_DEFAULT_JWT_SECRET, + DEFAULT_LOCAL_DATABASE_URL, +} from './validate-required-config'; + +const prodConfig = (over: Partial = {}): RequiredConfig => + ({ + env: 'production', + port: 3000, + jwt: { secret: 'a-real-secret' }, + database: { url: 'postgresql://prod-host/db' }, + github: { + clientId: 'cid', + clientSecret: 'csec', + webhookSecret: 'whsec', + }, + stellar: { + escrowContractId: 'CESCROW', + treasurySecret: 'STREAS', + }, + ...over, + }) as RequiredConfig; + +describe('collectConfigIssues', () => { + it('returns no issues for a complete production config', () => { + expect(collectConfigIssues(prodConfig())).toEqual([]); + }); + + it('returns no issues outside production', () => { + const dev = prodConfig({ + env: 'development', + jwt: { secret: '' }, + github: { clientId: '', clientSecret: '', webhookSecret: '' }, + } as Partial); + expect(collectConfigIssues(dev)).toEqual([]); + }); + + it('flags the insecure default JWT secret', () => { + const issues = collectConfigIssues( + prodConfig({ jwt: { secret: INSECURE_DEFAULT_JWT_SECRET } } as any), + ); + expect(issues.some((i) => i.key === 'JWT_SECRET')).toBe(true); + }); + + it('flags a missing/blank JWT secret', () => { + const issues = collectConfigIssues( + prodConfig({ jwt: { secret: ' ' } } as any), + ); + expect(issues.some((i) => i.key === 'JWT_SECRET')).toBe(true); + }); + + it('flags the default local DATABASE_URL', () => { + const issues = collectConfigIssues( + prodConfig({ database: { url: DEFAULT_LOCAL_DATABASE_URL } } as any), + ); + expect(issues.some((i) => i.key === 'DATABASE_URL')).toBe(true); + }); + + it('flags missing escrow contract + treasury secret', () => { + const issues = collectConfigIssues( + prodConfig({ + stellar: { escrowContractId: '', treasurySecret: '' }, + } as any), + ); + const keys = issues.map((i) => i.key); + expect(keys).toContain('ESCROW_CONTRACT_ID'); + expect(keys).toContain('TREASURY_SECRET'); + }); +}); + +describe('assertRequiredConfig', () => { + it('does not throw for a valid production config', () => { + expect(() => assertRequiredConfig(prodConfig())).not.toThrow(); + }); + + it('throws listing every problem at once', () => { + expect(() => + assertRequiredConfig( + prodConfig({ + jwt: { secret: '' }, + stellar: { escrowContractId: '', treasurySecret: '' }, + } as any), + ), + ).toThrow(/Refusing to start/); + }); +}); From dec45fcfb3a66aacff74b0db4bfa6e8fb945025c Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:50:10 -0400 Subject: [PATCH 2/2] fix: repair merge glue that left the branch uncompilable The shared base carried half-written merge artifacts: an orphaned invokeOnLockedEscrow block in escrow.service.ts, duplicated returns plus a dead parsePagination in sponsors.controller.ts, and missing describe/it closers in github-webhooks.service.spec.ts. Aligned each with the intended form so the branch typechecks and its specs load. --- src/escrow/escrow.service.ts | 10 ++-------- src/github/github-webhooks.service.spec.ts | 3 +++ src/sponsors/sponsors.controller.ts | 21 --------------------- 3 files changed, 5 insertions(+), 29 deletions(-) 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)); } }