Skip to content

Commit 8c57504

Browse files
authored
Merge pull request #445 from christabel888/fix/payments-limits-webhooks-improvements
feat: request id propagation, feature flag guards, webhook validation and filtering
2 parents f6b9353 + 9c6e879 commit 8c57504

18 files changed

Lines changed: 497 additions & 48 deletions

src/common/middleware/request-logging.middleware.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import requestLogger from './request-logging.middleware';
22
import { Logger } from '@nestjs/common';
3+
import { RequestContextService } from '../request-context/request-context.service';
34

45
describe('requestLogger', () => {
56
beforeEach(() => jest.restoreAllMocks());
@@ -128,4 +129,32 @@ describe('requestLogger', () => {
128129

129130
expect(req.requestId).toBe(existingId);
130131
});
132+
133+
it('propagates request ID into RequestContextService async context', () => {
134+
const req: any = {
135+
method: 'GET',
136+
originalUrl: '/test',
137+
headers: {},
138+
ip: '1.2.3.4',
139+
};
140+
const res: any = {
141+
setHeader: jest.fn(),
142+
on: jest.fn(),
143+
statusCode: 200,
144+
};
145+
146+
let capturedRequestId: string | undefined;
147+
const next = jest.fn().mockImplementation(() => {
148+
const service = new RequestContextService();
149+
capturedRequestId = service.getRequestId();
150+
});
151+
152+
jest.spyOn(Logger.prototype, 'log').mockImplementation(() => {});
153+
154+
requestLogger(req, res, next as any);
155+
156+
expect(next).toHaveBeenCalled();
157+
expect(capturedRequestId).toBeDefined();
158+
expect(capturedRequestId).toBe(req.requestId);
159+
});
131160
});

src/common/middleware/request-logging.middleware.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import { Request, Response, NextFunction } from 'express';
22
import { Logger } from '@nestjs/common';
33
import { randomUUID } from 'crypto';
4+
import { RequestContextService } from '../request-context/request-context.service';
45

56
export function requestLogger(
67
req: Request | any,
78
res: Response | any,
89
next: NextFunction,
910
) {
1011
const logger = new Logger('RequestLogger');
11-
try {
12-
if (!req) {
13-
logger.warn('Request logging skipped: invalid request object');
14-
next();
15-
return;
16-
}
1712

13+
if (!req) {
14+
logger.warn('Request logging skipped: invalid request object');
15+
next();
16+
return;
17+
}
18+
19+
try {
1820
const idHeader =
19-
req &&
2021
req.headers &&
2122
(req.headers['x-request-id'] || req.headers['X-Request-Id']);
2223
const id =
@@ -25,10 +26,7 @@ export function requestLogger(
2526
: randomUUID();
2627
const start = Date.now();
2728

28-
// Attach request ID to request object for access in controllers/services
29-
if (req) {
30-
req.requestId = id;
31-
}
29+
req.requestId = id;
3230

3331
if (res && typeof res.setHeader === 'function') {
3432
try {
@@ -39,10 +37,9 @@ export function requestLogger(
3937
}
4038

4139
const ip =
42-
(req && (req.ip || (req.socket && req.socket.remoteAddress))) ||
43-
'unknown';
44-
const method = (req && req.method) || 'UNKNOWN';
45-
const url = (req && (req.originalUrl || req.url)) || 'unknown';
40+
(req.ip || (req.socket && req.socket.remoteAddress)) || 'unknown';
41+
const method = req.method || 'UNKNOWN';
42+
const url = (req.originalUrl || req.url) || 'unknown';
4643

4744
logger.log(`${method} ${url} id=${id} ip=${ip}`);
4845

@@ -58,13 +55,20 @@ export function requestLogger(
5855
}
5956
});
6057
}
58+
59+
RequestContextService.run({ requestId: id }, () => {
60+
try {
61+
next();
62+
} catch (e) {
63+
logger.warn('next() threw in requestLogger');
64+
}
65+
});
6166
} catch (err: any) {
6267
logger.warn('Request logging failed: ' + (err && err.message));
63-
} finally {
6468
try {
6569
next();
6670
} catch (e) {
67-
logger.warn('next() threw in requestLogger');
71+
logger.warn('next() threw after requestLogger error');
6872
}
6973
}
7074
}

src/limits/limits.controller.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LimitsController } from './limits.controller';
44
import { LimitsService } from './limits.service';
55
import { ApiKeyGuard } from '../api-keys/api-key.guard';
66
import { LimitPeriod } from './dto/create-limit.dto';
7+
import { FeatureFlagGuard } from '../common/feature-flags/feature-flag.guard';
78

89
const mockLimit = {
910
id: 'uuid-limit-1',
@@ -33,7 +34,10 @@ describe('LimitsController', () => {
3334
const module: TestingModule = await Test.createTestingModule({
3435
controllers: [LimitsController],
3536
providers: [{ provide: LimitsService, useValue: limitsService }],
36-
}).compile();
37+
})
38+
.overrideGuard(FeatureFlagGuard)
39+
.useValue({ canActivate: () => true })
40+
.compile();
3741

3842
controller = module.get<LimitsController>(LimitsController);
3943
});

src/limits/limits.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Delete,
88
HttpCode,
99
HttpStatus,
10+
UseGuards,
1011
} from '@nestjs/common';
1112
import {
1213
ApiTags,
@@ -17,9 +18,15 @@ import {
1718
} from '@nestjs/swagger';
1819
import { LimitsService } from './limits.service';
1920
import { SetLimitsDto } from './dto/set-limits.dto';
21+
import {
22+
FeatureFlagGuard,
23+
FeatureFlag,
24+
} from '../common/feature-flags/feature-flag.guard';
2025

2126
@ApiTags('limits')
2227
@Controller('wallets/:walletId/limits')
28+
@UseGuards(FeatureFlagGuard)
29+
@FeatureFlag('limits_api')
2330
export class LimitsController {
2431
constructor(private readonly limitsService: LimitsService) {}
2532

src/limits/limits.module.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
23
import { LimitsService } from './limits.service';
34
import { LimitsController } from './limits.controller';
45
import { PrismaModule } from '../prisma/prisma.module';
6+
import { RequestContextService } from '../common/request-context/request-context.service';
7+
import { FeatureFlagService } from '../common/feature-flags/feature-flag.service';
8+
import { FeatureFlagGuard } from '../common/feature-flags/feature-flag.guard';
59

610
@Module({
7-
imports: [PrismaModule],
11+
imports: [ConfigModule, PrismaModule],
812
controllers: [LimitsController],
9-
providers: [LimitsService],
13+
providers: [
14+
LimitsService,
15+
RequestContextService,
16+
FeatureFlagService,
17+
FeatureFlagGuard,
18+
],
1019
exports: [LimitsService],
1120
})
1221
export class LimitsModule {}

src/limits/limits.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,17 @@ export class LimitsService {
120120
}
121121

122122
async checkLimits(walletId: string, amount: number): Promise<void> {
123+
const requestId = this.requestContext.getRequestId();
123124
const limits = await this.getLimits(walletId);
124125
if (!limits) {
125126
this.metrics.incrementLimitChecks('allowed');
126127
return;
127128
}
128129

130+
this.logger.log(
131+
`Checking limits walletId=${walletId} amount=${amount} requestId=${requestId}`,
132+
);
133+
129134
// Enforce per-transaction cap: a cap of 0 blocks all transactions
130135
if (
131136
limits.perTransactionLimit >= 0 &&

src/payments/payments-limits.integration.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WalletsService } from '../wallets/wallets.service';
66
import { PrismaService } from '../prisma/prisma.service';
77
import { PaymentStatus } from './entities/payment.entity';
88
import { WalletStatus } from '../wallets/domain/wallet.model';
9+
import { RequestContextService } from '../common/request-context/request-context.service';
910

1011
describe('Payments and Limits Integration', () => {
1112
let paymentsService: PaymentsService;
@@ -30,6 +31,10 @@ describe('Payments and Limits Integration', () => {
3031
findWalletById: jest.fn(),
3132
};
3233

34+
const mockRequestContext = {
35+
getRequestId: jest.fn().mockReturnValue('integration-req-id'),
36+
};
37+
3338
beforeEach(async () => {
3439
jest.clearAllMocks();
3540

@@ -39,6 +44,7 @@ describe('Payments and Limits Integration', () => {
3944
LimitsService,
4045
{ provide: PrismaService, useValue: mockPrisma },
4146
{ provide: WalletsService, useValue: mockWalletsService },
47+
{ provide: RequestContextService, useValue: mockRequestContext },
4248
],
4349
}).compile();
4450

src/payments/payments.controller.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { PaymentsController } from './payments.controller';
44
import { PaymentsService } from './payments.service';
55
import { ApiKeyGuard } from '../api-keys/api-key.guard';
66
import { RateLimitGuard } from '../rate-limit/rate-limit.guard';
7+
import { FeatureFlagGuard } from '../common/feature-flags/feature-flag.guard';
78
import { PaymentStatus } from './entities/payment.entity';
89

910
describe('PaymentsController', () => {
@@ -27,6 +28,8 @@ describe('PaymentsController', () => {
2728
.useValue({ canActivate: () => true })
2829
.overrideGuard(RateLimitGuard)
2930
.useValue({ canActivate: () => true })
31+
.overrideGuard(FeatureFlagGuard)
32+
.useValue({ canActivate: () => true })
3033
.compile();
3134

3235
controller = module.get<PaymentsController>(PaymentsController);
@@ -77,6 +80,30 @@ describe('PaymentsController', () => {
7780
});
7881
});
7982

83+
describe('feature flag guard', () => {
84+
it('should deny access when feature flag is disabled', async () => {
85+
const restrictedModule = await Test.createTestingModule({
86+
controllers: [PaymentsController],
87+
providers: [{ provide: PaymentsService, useValue: paymentsService }],
88+
})
89+
.overrideGuard(ApiKeyGuard)
90+
.useValue({ canActivate: () => true })
91+
.overrideGuard(RateLimitGuard)
92+
.useValue({ canActivate: () => true })
93+
.overrideGuard(FeatureFlagGuard)
94+
.useValue({
95+
canActivate: () => {
96+
throw new Error('Feature not available');
97+
},
98+
})
99+
.compile();
100+
101+
const restrictedController =
102+
restrictedModule.get<PaymentsController>(PaymentsController);
103+
expect(restrictedController).toBeDefined();
104+
});
105+
});
106+
80107
describe('swagger decorators', () => {
81108
it('should have @ApiResponse decorators on all routes', () => {
82109
const routes = ['create', 'findAll', 'findOne', 'update', 'remove'];

src/payments/payments.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ import {
2727
SensitiveEndpoint,
2828
} from '../rate-limit/rate-limit.guard';
2929
import { PaginationDto } from '../common/dto/pagination.dto';
30+
import {
31+
FeatureFlagGuard,
32+
FeatureFlag,
33+
} from '../common/feature-flags/feature-flag.guard';
3034

3135
@ApiTags('payments')
3236
@Controller('payments')
33-
@UseGuards(ApiKeyGuard, RateLimitGuard)
37+
@UseGuards(ApiKeyGuard, RateLimitGuard, FeatureFlagGuard)
38+
@FeatureFlag('payments_api')
3439
export class PaymentsController {
3540
constructor(private readonly paymentsService: PaymentsService) {}
3641

src/payments/payments.module.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { Module } from '@nestjs/common';
2+
import { ConfigModule } from '@nestjs/config';
23
import { PaymentsService } from './payments.service';
34
import { PaymentsController } from './payments.controller';
45
import { LimitsModule } from '../limits/limits.module';
56
import { WalletsModule } from '../wallets/wallets.module';
7+
import { RequestContextService } from '../common/request-context/request-context.service';
8+
import { FeatureFlagService } from '../common/feature-flags/feature-flag.service';
9+
import { FeatureFlagGuard } from '../common/feature-flags/feature-flag.guard';
610

711
@Module({
8-
imports: [LimitsModule, WalletsModule],
12+
imports: [ConfigModule, LimitsModule, WalletsModule],
913
controllers: [PaymentsController],
10-
providers: [PaymentsService],
14+
providers: [
15+
PaymentsService,
16+
RequestContextService,
17+
FeatureFlagService,
18+
FeatureFlagGuard,
19+
],
1120
})
1221
export class PaymentsModule {}

0 commit comments

Comments
 (0)