Skip to content

Commit a2991c8

Browse files
committed
test: adapt the suite to the real database and restore green checks
1 parent 04685d8 commit a2991c8

67 files changed

Lines changed: 2604 additions & 1791 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.

src/admin/api-keys/api-keys.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import {
66
Patch,
77
UseGuards,
88
} from '@nestjs/common';
9-
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
9+
import {
10+
ApiBearerAuth,
11+
ApiOperation,
12+
ApiResponse,
13+
ApiTags,
14+
} from '@nestjs/swagger';
1015
import { Throttle } from '@nestjs/throttler';
1116
import { JwtGuard } from '../../auth/guards/jwt.guard';
1217
import { AdminGuard } from '../guards/admin.guard';

src/admin/dispute/dispute.controller.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
2-
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
1+
import {
2+
Body,
3+
Controller,
4+
Get,
5+
Param,
6+
Patch,
7+
Query,
8+
UseGuards,
9+
} from '@nestjs/common';
10+
import {
11+
ApiBearerAuth,
12+
ApiOperation,
13+
ApiResponse,
14+
ApiTags,
15+
} from '@nestjs/swagger';
316
import { Throttle } from '@nestjs/throttler';
417
import { JwtGuard } from '../../auth/guards/jwt.guard';
518
import { CurrentUser } from '../../auth/decorators/current-user.decorator';

src/admin/queues/queue-dashboard.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Controller, Get, UseGuards } from '@nestjs/common';
2-
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
2+
import {
3+
ApiBearerAuth,
4+
ApiOperation,
5+
ApiResponse,
6+
ApiTags,
7+
} from '@nestjs/swagger';
38
import { Throttle } from '@nestjs/throttler';
49
import { JwtGuard } from '../../auth/guards/jwt.guard';
510
import { AdminGuard } from '../guards/admin.guard';

src/admin/stats/admin-stats.controller.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Controller, Get, UseGuards } from '@nestjs/common';
2-
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
2+
import {
3+
ApiBearerAuth,
4+
ApiOperation,
5+
ApiResponse,
6+
ApiTags,
7+
} from '@nestjs/swagger';
38
import { Throttle } from '@nestjs/throttler';
49
import { JwtGuard } from '../../auth/guards/jwt.guard';
510
import { AdminGuard } from '../guards/admin.guard';

src/admin/stats/admin-stats.service.spec.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AdminStatsService } from './admin-stats.service';
22
import { PrismaService } from '../../prisma/prisma.service';
3+
import { ensureVendors } from '../../../test/prisma-helpers';
34

45
describe('AdminStatsService', () => {
56
let service: AdminStatsService;
@@ -8,9 +9,24 @@ describe('AdminStatsService', () => {
89
beforeEach(async () => {
910
prisma = new PrismaService();
1011
await prisma.reset();
12+
// Escrow.vendorAddress is a foreign key onto VendorProfile.address (#475).
13+
await ensureVendors(
14+
prisma,
15+
'GVENDOR1',
16+
'GVENDOR2',
17+
'GVENDOR_A',
18+
'GVENDOR_B',
19+
);
1120
service = new AdminStatsService(prisma);
1221
});
1322

23+
afterEach(async () => {
24+
// Each `new PrismaService()` opens its own connection pool. Constructed in
25+
// beforeEach across ~100 suites, undisconnected clients exhaust Postgres
26+
// (`sorry, too many clients already`) partway through a full run.
27+
await prisma?.$disconnect();
28+
});
29+
1430
afterEach(async () => {
1531
await prisma.reset();
1632
});
@@ -206,9 +222,23 @@ describe('AdminStatsService', () => {
206222
},
207223
});
208224

225+
// A third dispute needs a third escrow: Dispute.escrowId is unique, so a
226+
// second dispute on `escrow` is impossible against the real schema (#475).
227+
const escrow3 = await prisma.escrow.create({
228+
data: {
229+
itemName: 'Item 3',
230+
itemRef: 'ref-stats-3',
231+
amount: 50,
232+
currency: 'USDC',
233+
buyerAddress: 'GBUYER1',
234+
vendorAddress: 'GVENDOR1',
235+
state: 'COMPLETED',
236+
},
237+
});
238+
209239
await prisma.dispute.create({
210240
data: {
211-
escrowId: escrow.id,
241+
escrowId: escrow3.id,
212242
reason: 'Already resolved',
213243
status: 'RESOLVED',
214244
},

src/common/sanitization/credential-encryption.util.spec.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ describe('CredentialEncryption', () => {
2525
expect(encrypted1).not.toBe(encrypted2);
2626
});
2727

28-
29-
3028
it('should throw when encryption key has invalid length', () => {
31-
3229
expect(() => encryptCredential('test', 'short-key')).toThrow(
3330
'Encryption key must be exactly 64 hex characters',
3431
);
@@ -72,9 +69,9 @@ describe('CredentialEncryption', () => {
7269
// Create a malformed encrypted string with wrong IV length
7370
const malformedEncrypted = 'short-iv:tag:ciphertext';
7471

75-
expect(() => decryptCredential(malformedEncrypted, encryptionKey)).toThrow(
76-
'Malformed encrypted credential',
77-
);
72+
expect(() =>
73+
decryptCredential(malformedEncrypted, encryptionKey),
74+
).toThrow('Malformed encrypted credential');
7875
});
7976

8077
it('should throw when decryption fails (tampered data)', () => {
@@ -89,15 +86,17 @@ describe('CredentialEncryption', () => {
8986
'Failed to decrypt credential',
9087
);
9188
});
92-
93-
9489
});
9590

9691
describe('reencryptCredential', () => {
9792
it('should re-encrypt a credential with the same key', () => {
9893
const plaintext = 'my-secret-api-key-12345';
9994
const encrypted1 = encryptCredential(plaintext, encryptionKey);
100-
const reencrypted = reencryptCredential(encrypted1, encryptionKey, encryptionKey);
95+
const reencrypted = reencryptCredential(
96+
encrypted1,
97+
encryptionKey,
98+
encryptionKey,
99+
);
101100
const decrypted = decryptCredential(reencrypted, encryptionKey);
102101

103102
expect(reencrypted).not.toBe(encrypted1);
@@ -107,18 +106,26 @@ describe('CredentialEncryption', () => {
107106
it('should produce different ciphertext on each re-encryption', () => {
108107
const plaintext = 'my-secret-api-key-12345';
109108
const encrypted = encryptCredential(plaintext, encryptionKey);
110-
const reencrypted1 = reencryptCredential(encrypted, encryptionKey, encryptionKey);
111-
const reencrypted2 = reencryptCredential(encrypted, encryptionKey, encryptionKey);
109+
const reencrypted1 = reencryptCredential(
110+
encrypted,
111+
encryptionKey,
112+
encryptionKey,
113+
);
114+
const reencrypted2 = reencryptCredential(
115+
encrypted,
116+
encryptionKey,
117+
encryptionKey,
118+
);
112119

113120
expect(reencrypted1).not.toBe(reencrypted2);
114121
});
115122

116123
it('should throw when encrypted format is invalid', () => {
117124
const invalidEncrypted = 'invalid-format';
118125

119-
expect(() => reencryptCredential(invalidEncrypted, encryptionKey, encryptionKey)).toThrow(
120-
'Invalid encrypted credential format',
121-
);
126+
expect(() =>
127+
reencryptCredential(invalidEncrypted, encryptionKey, encryptionKey),
128+
).toThrow('Invalid encrypted credential format');
122129
});
123130
});
124131

src/common/sanitization/credential-encryption.util.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ const SEPARATOR = ':';
1212
* A fresh IV is generated per call so encrypting the same value twice
1313
* produces different ciphertext — prevents correlation attacks.
1414
*/
15-
export function encryptCredential(plaintext: string, encryptionKey: string): string {
15+
export function encryptCredential(
16+
plaintext: string,
17+
encryptionKey: string,
18+
): string {
1619
const key = Buffer.from(encryptionKey, 'hex');
1720
if (key.length !== 32) {
1821
throw new Error(
@@ -40,7 +43,10 @@ export function encryptCredential(plaintext: string, encryptionKey: string): str
4043
* Decrypts a value produced by `encryptCredential`.
4144
* Returns the plaintext string, or throws if the ciphertext is tampered.
4245
*/
43-
export function decryptCredential(stored: string, encryptionKey: string): string {
46+
export function decryptCredential(
47+
stored: string,
48+
encryptionKey: string,
49+
): string {
4450
const key = Buffer.from(encryptionKey, 'hex');
4551
if (key.length !== 32) {
4652
throw new Error(
@@ -79,7 +85,11 @@ export function decryptCredential(stored: string, encryptionKey: string): string
7985
* Re-encrypts a credential with a new key.
8086
* This is used during key rotation operations.
8187
*/
82-
export function reencryptCredential(encryptedCredential: string, oldKey: string, newKey: string): string {
88+
export function reencryptCredential(
89+
encryptedCredential: string,
90+
oldKey: string,
91+
newKey: string,
92+
): string {
8393
const plaintext = decryptCredential(encryptedCredential, oldKey);
8494
return encryptCredential(plaintext, newKey);
8595
}

src/common/security/cors-origin.spec.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ describe('CORS origin callback (disallowed origin → clean denial)', () => {
8080
.set('Origin', DISALLOWED_ORIGIN);
8181

8282
expect(response.status).not.toBe(500);
83-
expect(
84-
response.headers['access-control-allow-origin'],
85-
).toBeUndefined();
83+
expect(response.headers['access-control-allow-origin']).toBeUndefined();
8684
});
8785

8886
it('allows a request with no Origin header (non-browser clients)', async () => {
@@ -99,9 +97,7 @@ describe('CORS origin callback (disallowed origin → clean denial)', () => {
9997
.set('Access-Control-Request-Method', 'GET');
10098

10199
expect(response.status).toBe(204);
102-
const advertised = (
103-
response.headers['access-control-allow-headers'] ?? ''
104-
)
100+
const advertised = (response.headers['access-control-allow-headers'] ?? '')
105101
.split(',')
106102
.map((h: string) => h.trim().toLowerCase());
107103
expect(advertised).toContain('idempotency-key');
@@ -114,8 +110,6 @@ describe('CORS origin callback (disallowed origin → clean denial)', () => {
114110
.set('Access-Control-Request-Method', 'GET');
115111

116112
expect(response.status).not.toBe(500);
117-
expect(
118-
response.headers['access-control-allow-origin'],
119-
).toBeUndefined();
113+
expect(response.headers['access-control-allow-origin']).toBeUndefined();
120114
});
121115
});

src/dispute/dispute.repository.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ describe('DisputeRepository', () => {
99

1010
beforeEach(async () => {
1111
prisma = new PrismaService();
12+
// State lives in a shared database now, not a per-instance Map, so a
13+
// suite that does not clear it inherits whatever the previous file left
14+
// behind — and jest's file ordering is not stable (#475).
15+
await prisma.reset();
1216
await prisma.vendorProfile.createMany({
1317
data: [{ address: 'vendor', businessName: 'Test Vendor' }],
1418
skipDuplicates: true,
@@ -17,6 +21,13 @@ describe('DisputeRepository', () => {
1721
escrowRepo = new EscrowRepository(prisma);
1822
});
1923

24+
afterEach(async () => {
25+
// Each `new PrismaService()` opens its own connection pool. Constructed in
26+
// beforeEach across ~100 suites, undisconnected clients exhaust Postgres
27+
// (`sorry, too many clients already`) partway through a full run.
28+
await prisma?.$disconnect();
29+
});
30+
2031
describe('findByEscrow()', () => {
2132
it('returns the dispute linked to the given escrow', async () => {
2233
const escrow = await escrowRepo.create(

src/dlq/dlq.controller.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,27 @@ export class DlqController {
9292
type: Number,
9393
description: 'Maximum records per page (default 20, max 100)',
9494
})
95-
@ApiOperation({ summary: 'List failed contract transactions available for review and replay' })
96-
@ApiResponse({ status: 200, description: 'Failed transaction records returned.' })
95+
@ApiOperation({
96+
summary:
97+
'List failed contract transactions available for review and replay',
98+
})
99+
@ApiResponse({
100+
status: 200,
101+
description: 'Failed transaction records returned.',
102+
})
97103
@ApiResponse({ status: 401, description: 'Unauthorized.' })
98104
@ApiResponse({ status: 403, description: 'Admin access required.' })
99105
@ApiQuery({ name: 'status', required: false, example: 'failed' })
100-
@ApiQuery({ name: 'operation', required: false, example: 'submitAutoRelease' })
101-
@ApiQuery({ name: 'escrowId', required: false, example: '9d9e2e16-0c78-4a84-9c8c-0f3a5eb2d4e3' })
106+
@ApiQuery({
107+
name: 'operation',
108+
required: false,
109+
example: 'submitAutoRelease',
110+
})
111+
@ApiQuery({
112+
name: 'escrowId',
113+
required: false,
114+
example: '9d9e2e16-0c78-4a84-9c8c-0f3a5eb2d4e3',
115+
})
102116
@Throttle({ auth: { limit: 20, ttl: 60000 } })
103117
@Get()
104118
list(
@@ -118,10 +132,16 @@ export class DlqController {
118132
}
119133

120134
@ApiOperation({ summary: 'Get details for a failed transaction record' })
121-
@ApiResponse({ status: 200, description: 'Failed transaction record returned.' })
135+
@ApiResponse({
136+
status: 200,
137+
description: 'Failed transaction record returned.',
138+
})
122139
@ApiResponse({ status: 401, description: 'Unauthorized.' })
123140
@ApiResponse({ status: 403, description: 'Admin access required.' })
124-
@ApiResponse({ status: 404, description: 'Failed transaction record not found.' })
141+
@ApiResponse({
142+
status: 404,
143+
description: 'Failed transaction record not found.',
144+
})
125145
@ApiParam({ name: 'id', example: 'abc123-def4-5678-90ab-cdef12345678' })
126146
@Throttle({ auth: { limit: 30, ttl: 60000 } })
127147
@Get(':id')
@@ -135,10 +155,16 @@ export class DlqController {
135155
* hand. Either way the record is updated on the outcome.
136156
*/
137157
@ApiOperation({ summary: 'Replay a failed on-chain transaction attempt' })
138-
@ApiResponse({ status: 200, description: 'Replay request accepted and replay execution started.' })
158+
@ApiResponse({
159+
status: 200,
160+
description: 'Replay request accepted and replay execution started.',
161+
})
139162
@ApiResponse({ status: 401, description: 'Unauthorized.' })
140163
@ApiResponse({ status: 403, description: 'Admin access required.' })
141-
@ApiResponse({ status: 404, description: 'Failed transaction record not found.' })
164+
@ApiResponse({
165+
status: 404,
166+
description: 'Failed transaction record not found.',
167+
})
142168
@ApiParam({ name: 'id', example: 'abc123-def4-5678-90ab-cdef12345678' })
143169
@Throttle({ auth: { limit: 5, ttl: 60000 } })
144170
@Post(':id/replay')
@@ -157,11 +183,20 @@ export class DlqController {
157183
});
158184
}
159185

160-
@ApiOperation({ summary: 'Abandon a failed transaction record and prevent future replay attempts' })
161-
@ApiResponse({ status: 200, description: 'Failed transaction record abandoned.' })
186+
@ApiOperation({
187+
summary:
188+
'Abandon a failed transaction record and prevent future replay attempts',
189+
})
190+
@ApiResponse({
191+
status: 200,
192+
description: 'Failed transaction record abandoned.',
193+
})
162194
@ApiResponse({ status: 401, description: 'Unauthorized.' })
163195
@ApiResponse({ status: 403, description: 'Admin access required.' })
164-
@ApiResponse({ status: 404, description: 'Failed transaction record not found.' })
196+
@ApiResponse({
197+
status: 404,
198+
description: 'Failed transaction record not found.',
199+
})
165200
@ApiParam({ name: 'id', example: 'abc123-def4-5678-90ab-cdef12345678' })
166201
@Throttle({ auth: { limit: 5, ttl: 60000 } })
167202
@Post(':id/abandon')

0 commit comments

Comments
 (0)