fix(tests): resolve 4 failing test suites and patch lodash vulnerability
Test fixes (type mismatches introduced by V&V resolution changes):
- HealthDetailedController.test.ts: replace pool/makePool with dbProbe/makeDbProbe
to match refactored HealthDetailedDeps interface (Pool → DbProbe abstraction)
- EventPublisher.test.ts: pass all 4 required constructor args to WebhookDeliveryWorker
mock (pool, vaultClient, redisClient, redisUrl) — was passing only 1
- MarketplaceService.test.ts: IAgent.did/didCreatedAt are string|undefined (not null);
fix makeAgent defaults and makeAgent({did:null}) call; fix type assertion to unknown first
- OIDCTrustPolicyService.test.ts: ICreateTrustPolicyRequest.branch is string|undefined
(not nullable); replace all branch:null with branch:undefined
Security fix:
- npm audit fix: lodash ≤4.17.23 (HIGH) → patched; 0 vulnerabilities remaining
Result: 50/50 test suites pass, 722/722 tests pass, 0 vulnerabilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -6202,9 +6202,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
import express, { Application } from 'express';
|
import express, { Application } from 'express';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { Pool, PoolClient } from 'pg';
|
import { HealthDetailedController, HealthDetailedDeps, DbProbe } from '../../../src/controllers/HealthDetailedController';
|
||||||
import { HealthDetailedController, HealthDetailedDeps } from '../../../src/controllers/HealthDetailedController';
|
|
||||||
|
|
||||||
// ── fetch mock ────────────────────────────────────────────────────────────────
|
// ── fetch mock ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -22,23 +21,19 @@ global.fetch = mockFetch;
|
|||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function makePoolClient(latencyMs = 0, error?: Error): jest.Mocked<Pick<PoolClient, 'query' | 'release'>> {
|
/**
|
||||||
|
* Creates a mock DbProbe. When `error` is provided, checkLiveness() rejects
|
||||||
|
* with that error (simulates unreachable DB). Otherwise it resolves after
|
||||||
|
* `latencyMs` ms (0 by default — Date.now mocking handles degraded scenarios).
|
||||||
|
*/
|
||||||
|
function makeDbProbe(error?: Error, latencyMs = 0): DbProbe {
|
||||||
return {
|
return {
|
||||||
query: error
|
checkLiveness: error
|
||||||
? jest.fn().mockRejectedValue(error)
|
? jest.fn().mockRejectedValue(error)
|
||||||
: jest.fn().mockImplementation(() =>
|
: jest.fn().mockImplementation(
|
||||||
new Promise((resolve) => setTimeout(() => resolve({ rows: [], rowCount: 0 }), latencyMs)),
|
() => new Promise<void>((resolve) => setTimeout(() => resolve(), latencyMs)),
|
||||||
),
|
),
|
||||||
release: jest.fn(),
|
};
|
||||||
} as unknown as jest.Mocked<Pick<PoolClient, 'query' | 'release'>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makePool(connectError?: Error, queryLatencyMs = 0, queryError?: Error): jest.Mocked<Pool> {
|
|
||||||
return {
|
|
||||||
connect: connectError
|
|
||||||
? jest.fn().mockRejectedValue(connectError)
|
|
||||||
: jest.fn().mockResolvedValue(makePoolClient(queryLatencyMs, queryError)),
|
|
||||||
} as unknown as jest.Mocked<Pool>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRedisClient(pingError?: Error, latencyMs = 0): { ping(): Promise<string> } {
|
function makeRedisClient(pingError?: Error, latencyMs = 0): { ping(): Promise<string> } {
|
||||||
@@ -67,7 +62,7 @@ beforeEach(() => {
|
|||||||
describe('GET /health/detailed — all services healthy', () => {
|
describe('GET /health/detailed — all services healthy', () => {
|
||||||
it('returns 200 with overall status "healthy" when postgres and redis respond quickly', async () => {
|
it('returns 200 with overall status "healthy" when postgres and redis respond quickly', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(undefined, 10),
|
dbProbe: makeDbProbe(undefined, 10),
|
||||||
redisClient: makeRedisClient(undefined, 5),
|
redisClient: makeRedisClient(undefined, 5),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +76,7 @@ describe('GET /health/detailed — all services healthy', () => {
|
|||||||
|
|
||||||
it('includes version and uptime in the response body', async () => {
|
it('includes version and uptime in the response body', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,7 +88,7 @@ describe('GET /health/detailed — all services healthy', () => {
|
|||||||
|
|
||||||
it('includes latencyMs for each service', async () => {
|
it('includes latencyMs for each service', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,7 +141,7 @@ describe('GET /health/detailed — degraded scenario', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(undefined, 0),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(undefined, 0),
|
redisClient: makeRedisClient(undefined, 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,7 +172,7 @@ describe('GET /health/detailed — degraded scenario', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(undefined, 0),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(undefined, 0),
|
redisClient: makeRedisClient(undefined, 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -195,7 +190,7 @@ describe('GET /health/detailed — degraded scenario', () => {
|
|||||||
describe('GET /health/detailed — unreachable scenarios', () => {
|
describe('GET /health/detailed — unreachable scenarios', () => {
|
||||||
it('returns 503 when postgres connect() throws', async () => {
|
it('returns 503 when postgres connect() throws', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(new Error('ECONNREFUSED')),
|
dbProbe: makeDbProbe(new Error('ECONNREFUSED')),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,7 +203,7 @@ describe('GET /health/detailed — unreachable scenarios', () => {
|
|||||||
|
|
||||||
it('returns 503 when redis ping() throws', async () => {
|
it('returns 503 when redis ping() throws', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(new Error('Redis ECONNREFUSED')),
|
redisClient: makeRedisClient(new Error('Redis ECONNREFUSED')),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,7 +216,7 @@ describe('GET /health/detailed — unreachable scenarios', () => {
|
|||||||
|
|
||||||
it('returns 503 when both postgres and redis are unreachable', async () => {
|
it('returns 503 when both postgres and redis are unreachable', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(new Error('PG down')),
|
dbProbe: makeDbProbe(new Error('PG down')),
|
||||||
redisClient: makeRedisClient(new Error('Redis down')),
|
redisClient: makeRedisClient(new Error('Redis down')),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +232,7 @@ describe('GET /health/detailed — unreachable scenarios', () => {
|
|||||||
describe('GET /health/detailed — optional services omitted when not configured', () => {
|
describe('GET /health/detailed — optional services omitted when not configured', () => {
|
||||||
it('does not include vault in services when vaultAddr is not provided', async () => {
|
it('does not include vault in services when vaultAddr is not provided', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +243,7 @@ describe('GET /health/detailed — optional services omitted when not configured
|
|||||||
|
|
||||||
it('does not include opa in services when opaUrl is not provided', async () => {
|
it('does not include opa in services when opaUrl is not provided', async () => {
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,7 +258,7 @@ describe('GET /health/detailed — Vault and OPA probes', () => {
|
|||||||
mockFetch.mockResolvedValue(new Response(null, { status: 200 }));
|
mockFetch.mockResolvedValue(new Response(null, { status: 200 }));
|
||||||
|
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
vaultAddr: 'http://vault:8200',
|
vaultAddr: 'http://vault:8200',
|
||||||
});
|
});
|
||||||
@@ -278,7 +273,7 @@ describe('GET /health/detailed — Vault and OPA probes', () => {
|
|||||||
mockFetch.mockRejectedValue(new Error('Network failure'));
|
mockFetch.mockRejectedValue(new Error('Network failure'));
|
||||||
|
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
vaultAddr: 'http://vault:8200',
|
vaultAddr: 'http://vault:8200',
|
||||||
});
|
});
|
||||||
@@ -292,7 +287,7 @@ describe('GET /health/detailed — Vault and OPA probes', () => {
|
|||||||
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
|
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
|
||||||
|
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
opaUrl: 'http://opa:8181',
|
opaUrl: 'http://opa:8181',
|
||||||
});
|
});
|
||||||
@@ -307,7 +302,7 @@ describe('GET /health/detailed — Vault and OPA probes', () => {
|
|||||||
mockFetch.mockResolvedValue(new Response(null, { status: 503 }));
|
mockFetch.mockResolvedValue(new Response(null, { status: 503 }));
|
||||||
|
|
||||||
const app = buildApp({
|
const app = buildApp({
|
||||||
pool: makePool(),
|
dbProbe: makeDbProbe(),
|
||||||
redisClient: makeRedisClient(),
|
redisClient: makeRedisClient(),
|
||||||
opaUrl: 'http://opa:8181',
|
opaUrl: 'http://opa:8181',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,13 @@ function makePool(queryImpl?: jest.Mock): jest.Mocked<Pool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeWorker(): jest.Mocked<WebhookDeliveryWorker> {
|
function makeWorker(): jest.Mocked<WebhookDeliveryWorker> {
|
||||||
const worker = new MockWorker({} as never) as jest.Mocked<WebhookDeliveryWorker>;
|
// WebhookDeliveryWorker(pool, vaultClient, redisClient, redisUrl) — pass all required args
|
||||||
|
const worker = new MockWorker(
|
||||||
|
{} as never,
|
||||||
|
null,
|
||||||
|
{} as never,
|
||||||
|
'redis://localhost',
|
||||||
|
) as jest.Mocked<WebhookDeliveryWorker>;
|
||||||
worker.enqueue = jest.fn().mockResolvedValue(undefined);
|
worker.enqueue = jest.fn().mockResolvedValue(undefined);
|
||||||
return worker;
|
return worker;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ function makeAgent(overrides: Partial<IAgent> = {}): IAgent {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
createdAt: new Date('2026-01-01'),
|
createdAt: new Date('2026-01-01'),
|
||||||
updatedAt: new Date('2026-01-02'),
|
updatedAt: new Date('2026-01-02'),
|
||||||
did: null,
|
did: undefined,
|
||||||
didCreatedAt: null,
|
didCreatedAt: undefined,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ describe('MarketplaceService', () => {
|
|||||||
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
|
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
|
||||||
|
|
||||||
const result = await service.listPublicAgents(BASE_FILTERS);
|
const result = await service.listPublicAgents(BASE_FILTERS);
|
||||||
const card = result.data[0] as Record<string, unknown>;
|
const card = result.data[0] as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
expect(card['email']).toBeUndefined();
|
expect(card['email']).toBeUndefined();
|
||||||
expect(card['organizationId']).toBeUndefined();
|
expect(card['organizationId']).toBeUndefined();
|
||||||
@@ -79,7 +79,7 @@ describe('MarketplaceService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return null DID document when agent has no DID', async () => {
|
it('should return null DID document when agent has no DID', async () => {
|
||||||
const agent = makeAgent({ did: null });
|
const agent = makeAgent({ did: undefined });
|
||||||
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
|
agentRepo.findPublicAgents = jest.fn().mockResolvedValue({ agents: [agent], total: 1 });
|
||||||
|
|
||||||
const result = await service.listPublicAgents(BASE_FILTERS);
|
const result = await service.listPublicAgents(BASE_FILTERS);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('OIDCTrustPolicyService', () => {
|
|||||||
const result = await service.createTrustPolicy({
|
const result = await service.createTrustPolicy({
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
repository: 'acme/my-repo',
|
repository: 'acme/my-repo',
|
||||||
branch: null,
|
branch: undefined,
|
||||||
agentId: 'agent-001',
|
agentId: 'agent-001',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ describe('OIDCTrustPolicyService', () => {
|
|||||||
service.createTrustPolicy({
|
service.createTrustPolicy({
|
||||||
provider: 'gitlab' as never,
|
provider: 'gitlab' as never,
|
||||||
repository: 'acme/my-repo',
|
repository: 'acme/my-repo',
|
||||||
branch: null,
|
branch: undefined,
|
||||||
agentId: 'agent-001',
|
agentId: 'agent-001',
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(ValidationError);
|
).rejects.toThrow(ValidationError);
|
||||||
@@ -77,7 +77,7 @@ describe('OIDCTrustPolicyService', () => {
|
|||||||
service.createTrustPolicy({
|
service.createTrustPolicy({
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
repository: 'no-slash-here',
|
repository: 'no-slash-here',
|
||||||
branch: null,
|
branch: undefined,
|
||||||
agentId: 'agent-001',
|
agentId: 'agent-001',
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(ValidationError);
|
).rejects.toThrow(ValidationError);
|
||||||
@@ -88,7 +88,7 @@ describe('OIDCTrustPolicyService', () => {
|
|||||||
service.createTrustPolicy({
|
service.createTrustPolicy({
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
repository: 'acme/my-repo',
|
repository: 'acme/my-repo',
|
||||||
branch: null,
|
branch: undefined,
|
||||||
agentId: '',
|
agentId: '',
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(ValidationError);
|
).rejects.toThrow(ValidationError);
|
||||||
@@ -101,7 +101,7 @@ describe('OIDCTrustPolicyService', () => {
|
|||||||
service.createTrustPolicy({
|
service.createTrustPolicy({
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
repository: 'acme/my-repo',
|
repository: 'acme/my-repo',
|
||||||
branch: null,
|
branch: undefined,
|
||||||
agentId: 'nonexistent',
|
agentId: 'nonexistent',
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow(ValidationError);
|
).rejects.toThrow(ValidationError);
|
||||||
|
|||||||
Reference in New Issue
Block a user