fix(vv): resolve all 6 V&V issues — field trial unblocked

All findings from the inaugural LeadValidator audit resolved and
confirmed. Release gate: PASS.

VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering
all 20 route groups (46 endpoints documented in docs/openapi/)

VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts —
replaced pool.query shim with unknown[] + Object.defineProperty,
zero any types, eslint-disable suppressions removed

VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and
HealthDetailedController — injected AgentRepository/CredentialRepository
and DbProbe interface respectively; added CredentialRepository.findActiveClientId()

VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services —
ComplianceStatusStore, EventPublisher, MarketplaceService,
OIDCTrustPolicyService, UsageService

VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route
groups — analytics, billing, tiers, webhooks, marketplace,
oidc-trust-policies, oidc-token-exchange

VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4
OpenSpec archives — all archives now complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-07 04:52:47 +00:00
parent d216096dfb
commit 7441c9f298
49 changed files with 8954 additions and 70 deletions

View File

@@ -0,0 +1,148 @@
/**
* Integration tests for Analytics endpoints.
* Uses a real Postgres test DB and Redis test instance.
*
* Routes covered:
* GET /api/v1/analytics/tokens — daily token issuance trend
* GET /api/v1/analytics/agents/activity — agent activity heatmap
* GET /api/v1/analytics/agents — per-agent usage summary
*/
import crypto from 'crypto';
import request from 'supertest';
import { Application } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { Pool } from 'pg';
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
process.env['DATABASE_URL'] =
process.env['TEST_DATABASE_URL'] ??
'postgresql://sentryagent:sentryagent@localhost:5432/sentryagent_idp_test';
process.env['REDIS_URL'] = process.env['TEST_REDIS_URL'] ?? 'redis://localhost:6379/1';
process.env['JWT_PRIVATE_KEY'] = privateKey;
process.env['JWT_PUBLIC_KEY'] = publicKey;
process.env['NODE_ENV'] = 'test';
process.env['DEFAULT_ORG_ID'] = 'org_system';
process.env['ANALYTICS_ENABLED'] = 'true';
import { createApp } from '../../src/app';
import { signToken } from '../../src/utils/jwt';
import { closePool } from '../../src/db/pool';
import { closeRedisClient } from '../../src/cache/redis';
const ORG_ID = uuidv4();
const AGENT_ID = uuidv4();
const SCOPE = 'analytics:read';
function makeToken(sub: string = AGENT_ID, scope: string = SCOPE, orgId: string = ORG_ID): string {
return signToken({ sub, client_id: sub, scope, organization_id: orgId, jti: uuidv4() }, privateKey);
}
describe('Analytics Endpoints Integration Tests', () => {
let app: Application;
let pool: Pool;
beforeAll(async () => {
app = await createApp();
pool = new Pool({ connectionString: process.env['DATABASE_URL'] });
await pool.query(`
CREATE TABLE IF NOT EXISTS organizations (
organization_id VARCHAR(40) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
plan VARCHAR(20) NOT NULL DEFAULT 'free',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS analytics_events (
id BIGSERIAL PRIMARY KEY,
tenant_id VARCHAR(40) NOT NULL,
date DATE NOT NULL,
metric_type VARCHAR(50) NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
UNIQUE(tenant_id, date, metric_type)
)
`);
await pool.query(
`INSERT INTO organizations (organization_id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
[ORG_ID, 'Test Analytics Org'],
);
});
afterAll(async () => {
await pool.query(`DELETE FROM analytics_events WHERE tenant_id = $1`, [ORG_ID]);
await pool.query(`DELETE FROM organizations WHERE organization_id = $1`, [ORG_ID]);
await pool.end();
await closePool();
await closeRedisClient();
});
// ─── GET /analytics/tokens ──────────────────────────────────────────────────
describe('GET /api/v1/analytics/tokens', () => {
it('should return 200 with token trend data', async () => {
const res = await request(app)
.get('/api/v1/analytics/tokens')
.set('Authorization', `Bearer ${makeToken()}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('should return 401 when no token provided', async () => {
const res = await request(app).get('/api/v1/analytics/tokens');
expect(res.status).toBe(401);
});
it('should accept a ?days= query parameter', async () => {
const res = await request(app)
.get('/api/v1/analytics/tokens?days=7')
.set('Authorization', `Bearer ${makeToken()}`);
expect(res.status).toBe(200);
});
});
// ─── GET /analytics/agents/activity ─────────────────────────────────────────
describe('GET /api/v1/analytics/agents/activity', () => {
it('should return 200 with activity data', async () => {
const res = await request(app)
.get('/api/v1/analytics/agents/activity')
.set('Authorization', `Bearer ${makeToken()}`);
expect(res.status).toBe(200);
});
it('should return 401 when no token provided', async () => {
const res = await request(app).get('/api/v1/analytics/agents/activity');
expect(res.status).toBe(401);
});
});
// ─── GET /analytics/agents ───────────────────────────────────────────────────
describe('GET /api/v1/analytics/agents', () => {
it('should return 200 with agent usage summary', async () => {
const res = await request(app)
.get('/api/v1/analytics/agents')
.set('Authorization', `Bearer ${makeToken()}`);
expect(res.status).toBe(200);
});
it('should return 401 when no token provided', async () => {
const res = await request(app).get('/api/v1/analytics/agents');
expect(res.status).toBe(401);
});
});
});