Skip to content

Commit ad96406

Browse files
authored
Merge branch 'staging' into chore/payments-limits-boundaries-cache
2 parents b45de62 + ac52f89 commit ad96406

100 files changed

Lines changed: 8816 additions & 680 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

TEST_VERIFICATION_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ These test files were updated to use the new `/v1` prefix:
3333

3434
4. **test/wallets.e2e-spec.ts**
3535
- Tests wallet endpoint: `GET /v1/wallets/protected`
36+
- Tests wallet creation and wallet status paths
37+
- Verifies `x-request-id` propagation in headers
3638
- Verifies API key authentication with prefix
3739

3840
### New Test File

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,30 @@
2929
"@nestjs/common": "^11.0.1",
3030
"@nestjs/config": "^4.0.2",
3131
"@nestjs/core": "^11.0.1",
32+
"@nestjs/event-emitter": "^3.1.0",
3233
"@nestjs/mapped-types": "*",
3334
"@nestjs/platform-express": "^11.0.1",
3435
"@nestjs/terminus": "^11.1.1",
3536
"@nestjs/throttler": "^6.5.0",
3637
"@prisma/adapter-pg": "^7.3.0",
3738
"@prisma/client": "^7.3.0",
39+
"@willsoto/nestjs-prometheus": "^6.1.0",
3840
"axios": "^1.6.0",
3941
"class-transformer": "^0.5.1",
4042
"class-validator": "^0.15.1",
4143
"dotenv": "^17.2.3",
4244
"pg": "^8.17.2",
45+
"prom-client": "^15.1.3",
4346
"reflect-metadata": "^0.2.2",
4447
"rxjs": "^7.8.1",
4548
"stellar-sdk": "^10.2.0"
4649
},
4750
"devDependencies": {
4851
"@eslint/eslintrc": "^3.2.0",
4952
"@eslint/js": "^9.18.0",
50-
"@nestjs/swagger": "^8.0.0",
5153
"@nestjs/cli": "^11.0.0",
5254
"@nestjs/schematics": "^11.0.0",
55+
"@nestjs/swagger": "^8.0.0",
5356
"@nestjs/testing": "^11.0.1",
5457
"@types/express": "^5.0.0",
5558
"@types/jest": "^30.0.0",

pnpm-workspace.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
allowBuilds:
2+
'@nestjs/core': set this to true or false
3+
'@prisma/engines': set this to true or false
4+
'@scarf/scarf': set this to true or false
5+
prisma: set this to true or false
6+
sodium-native: set this to true or false
7+
unrs-resolver: set this to true or false

src/app.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { Module } from '@nestjs/common';
22
import { APP_GUARD } from '@nestjs/core';
33
import { AppController } from './app.controller';
44
import { ConfigModule } from '@nestjs/config';
5+
import { EventEmitterModule } from '@nestjs/event-emitter';
56
import { PrismaModule } from './prisma/prisma.module';
7+
import { MetricsModule } from './metrics/metrics.module';
68
import { AppService } from './app.service';
79
import { UsersModule } from './users/users.module';
810
import { IdempotentUserModule } from './users/idempotent-user.module';
@@ -29,7 +31,10 @@ import { HealthModule } from './health/health.module';
2931
ConfigModule.forRoot({
3032
isGlobal: true,
3133
envFilePath: '.env',
34+
validate: validateEnvironment,
3235
}),
36+
EventEmitterModule.forRoot(),
37+
MetricsModule,
3338
PrismaModule,
3439
AuthModule,
3540
RateLimitModule,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AuthOrchestrator } from './auth-orchestrator.service';
3+
import { IdempotentUserService } from '../users/idempotent-user.service';
4+
import { WalletCreationOrchestrator } from '../wallets/wallet-creation-orchestrator.service';
5+
import { IdempotencyService } from '../common/idempotency/idempotency.service';
6+
import { WebhookEventEmitterService } from '../webhooks/webhook-event-emitter.service';
7+
import { WalletNetwork, WalletStatus } from '../wallets/domain/wallet.model';
8+
9+
const NOW = new Date('2026-01-01T00:00:00.000Z');
10+
11+
const makeUser = (overrides: Record<string, any> = {}) => ({
12+
id: 'user-abc',
13+
authId: 'auth-abc',
14+
email: 'user@example.com',
15+
displayName: 'Test User',
16+
status: 'ACTIVE',
17+
authProvider: 'GOOGLE',
18+
lastLoginAt: NOW,
19+
createdAt: NOW,
20+
updatedAt: NOW,
21+
...overrides,
22+
});
23+
24+
const makeWallet = (overrides: Record<string, any> = {}) => ({
25+
id: 'wallet-abc',
26+
userId: 'user-abc',
27+
publicKey: 'GABC1234567890',
28+
encryptedSecret: 'enc',
29+
encryptionVersion: 1,
30+
secretVersion: 1,
31+
network: WalletNetwork.TESTNET,
32+
status: WalletStatus.ACTIVE,
33+
statusChangedAt: NOW,
34+
createdAt: NOW,
35+
updatedAt: NOW,
36+
rotatedFromId: null,
37+
statusReason: null,
38+
...overrides,
39+
});
40+
41+
describe('AuthOrchestrator — domain event emission', () => {
42+
let orchestrator: AuthOrchestrator;
43+
let webhookEventEmitter: jest.Mocked<WebhookEventEmitterService>;
44+
45+
const mockUserService = {
46+
findOrCreateUser: jest.fn(),
47+
findUserByAuthId: jest.fn(),
48+
listSessions: jest.fn(),
49+
};
50+
const mockWalletOrchestrator = {
51+
getWalletByUser: jest.fn(),
52+
createWallet: jest.fn(),
53+
};
54+
const mockIdempotencyService = {
55+
getCachedResponse: jest.fn().mockResolvedValue(null),
56+
cacheResponse: jest.fn().mockResolvedValue(undefined),
57+
};
58+
const mockWebhookEventEmitter = {
59+
emitUserAuthenticated: jest.fn().mockResolvedValue(undefined),
60+
emitNewUserRegistered: jest.fn().mockResolvedValue(undefined),
61+
emitAuthenticationFailed: jest.fn().mockResolvedValue(undefined),
62+
};
63+
64+
beforeEach(async () => {
65+
const module: TestingModule = await Test.createTestingModule({
66+
providers: [
67+
AuthOrchestrator,
68+
{ provide: IdempotentUserService, useValue: mockUserService },
69+
{ provide: WalletCreationOrchestrator, useValue: mockWalletOrchestrator },
70+
{ provide: IdempotencyService, useValue: mockIdempotencyService },
71+
{ provide: WebhookEventEmitterService, useValue: mockWebhookEventEmitter },
72+
],
73+
}).compile();
74+
75+
orchestrator = module.get<AuthOrchestrator>(AuthOrchestrator);
76+
webhookEventEmitter = module.get(WebhookEventEmitterService);
77+
jest.clearAllMocks();
78+
mockIdempotencyService.getCachedResponse.mockResolvedValue(null);
79+
mockIdempotencyService.cacheResponse.mockResolvedValue(undefined);
80+
mockWebhookEventEmitter.emitUserAuthenticated.mockResolvedValue(undefined);
81+
mockWebhookEventEmitter.emitNewUserRegistered.mockResolvedValue(undefined);
82+
mockWebhookEventEmitter.emitAuthenticationFailed.mockResolvedValue(undefined);
83+
});
84+
85+
it('emits auth.new_user_registered for first-time users', async () => {
86+
const user = makeUser();
87+
const wallet = makeWallet();
88+
89+
mockUserService.findOrCreateUser.mockResolvedValue({ user, isNewUser: true });
90+
mockWalletOrchestrator.getWalletByUser.mockResolvedValue(null);
91+
mockWalletOrchestrator.createWallet.mockResolvedValue({
92+
wallet,
93+
privateKey: 'secret',
94+
isNewWallet: true,
95+
});
96+
97+
await orchestrator.handleAuthentication({ authId: 'auth-abc' });
98+
99+
// Allow best-effort async emission to complete
100+
await new Promise((r) => setTimeout(r, 10));
101+
102+
expect(mockWebhookEventEmitter.emitNewUserRegistered).toHaveBeenCalledWith(
103+
expect.objectContaining({
104+
userId: user.id,
105+
authId: user.authId,
106+
authProvider: user.authProvider,
107+
walletId: wallet.id,
108+
walletNetwork: WalletNetwork.TESTNET,
109+
}),
110+
);
111+
expect(mockWebhookEventEmitter.emitUserAuthenticated).not.toHaveBeenCalled();
112+
});
113+
114+
it('emits auth.user_authenticated for returning users', async () => {
115+
const user = makeUser();
116+
const wallet = makeWallet();
117+
118+
mockUserService.findOrCreateUser.mockResolvedValue({ user, isNewUser: false });
119+
mockWalletOrchestrator.getWalletByUser.mockResolvedValue(wallet);
120+
121+
await orchestrator.handleAuthentication({ authId: 'auth-abc' });
122+
123+
await new Promise((r) => setTimeout(r, 10));
124+
125+
expect(mockWebhookEventEmitter.emitUserAuthenticated).toHaveBeenCalledWith(
126+
expect.objectContaining({
127+
userId: user.id,
128+
authId: user.authId,
129+
authProvider: user.authProvider,
130+
isNewWallet: false,
131+
}),
132+
);
133+
expect(mockWebhookEventEmitter.emitNewUserRegistered).not.toHaveBeenCalled();
134+
});
135+
136+
it('emits auth.authentication_failed on error', async () => {
137+
mockUserService.findOrCreateUser.mockRejectedValue(new Error('DB down'));
138+
139+
await expect(
140+
orchestrator.handleAuthentication({ authId: 'auth-abc' }),
141+
).rejects.toThrow('Authentication failed');
142+
143+
await new Promise((r) => setTimeout(r, 10));
144+
145+
expect(mockWebhookEventEmitter.emitAuthenticationFailed).toHaveBeenCalledWith(
146+
expect.objectContaining({ authId: 'auth-abc', reason: 'DB down' }),
147+
);
148+
});
149+
150+
it('does not throw if event emission fails (best-effort)', async () => {
151+
const user = makeUser();
152+
const wallet = makeWallet();
153+
154+
mockUserService.findOrCreateUser.mockResolvedValue({ user, isNewUser: false });
155+
mockWalletOrchestrator.getWalletByUser.mockResolvedValue(wallet);
156+
mockWebhookEventEmitter.emitUserAuthenticated.mockRejectedValue(
157+
new Error('webhook down'),
158+
);
159+
160+
await expect(
161+
orchestrator.handleAuthentication({ authId: 'auth-abc' }),
162+
).resolves.toBeDefined();
163+
});
164+
});

0 commit comments

Comments
 (0)