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>
160 lines
5.8 KiB
TypeScript
160 lines
5.8 KiB
TypeScript
/**
|
|
* Integration tests for Marketplace endpoints.
|
|
* Uses a real Postgres test DB and Redis test instance.
|
|
*
|
|
* Routes covered:
|
|
* GET /api/v1/marketplace — list public agents
|
|
* GET /api/v1/marketplace/:id — get a specific public agent
|
|
*
|
|
* Marketplace endpoints are unauthenticated (public listing).
|
|
*/
|
|
|
|
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';
|
|
|
|
import { createApp } from '../../src/app';
|
|
import { closePool } from '../../src/db/pool';
|
|
import { closeRedisClient } from '../../src/cache/redis';
|
|
|
|
const ORG_ID = uuidv4();
|
|
const PUBLIC_AGENT_ID = uuidv4();
|
|
const PRIVATE_AGENT_ID = uuidv4();
|
|
|
|
describe('Marketplace 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 agents (
|
|
agent_id VARCHAR(40) PRIMARY KEY,
|
|
organization_id VARCHAR(40) NOT NULL,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
agent_type VARCHAR(50) NOT NULL,
|
|
version VARCHAR(20) NOT NULL,
|
|
capabilities TEXT[] NOT NULL DEFAULT '{}',
|
|
owner VARCHAR(100) NOT NULL,
|
|
deployment_env VARCHAR(50) NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
is_public BOOLEAN NOT NULL DEFAULT false,
|
|
did TEXT,
|
|
did_created_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
)
|
|
`);
|
|
|
|
await pool.query(
|
|
`INSERT INTO organizations (organization_id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
[ORG_ID, 'Test Marketplace Org'],
|
|
);
|
|
|
|
// Insert a public agent
|
|
await pool.query(
|
|
`INSERT INTO agents
|
|
(agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status, is_public)
|
|
VALUES ($1, $2, $3, 'screener', 'v1.0.0', '{resume:read}', 'test-team', 'production', 'active', true)
|
|
ON CONFLICT DO NOTHING`,
|
|
[PUBLIC_AGENT_ID, ORG_ID, `public-${PUBLIC_AGENT_ID}@test.com`],
|
|
);
|
|
|
|
// Insert a private agent
|
|
await pool.query(
|
|
`INSERT INTO agents
|
|
(agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status, is_public)
|
|
VALUES ($1, $2, $3, 'screener', 'v1.0.0', '{resume:read}', 'test-team', 'production', 'active', false)
|
|
ON CONFLICT DO NOTHING`,
|
|
[PRIVATE_AGENT_ID, ORG_ID, `private-${PRIVATE_AGENT_ID}@test.com`],
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await pool.query(`DELETE FROM agents WHERE organization_id = $1`, [ORG_ID]);
|
|
await pool.query(`DELETE FROM organizations WHERE organization_id = $1`, [ORG_ID]);
|
|
await pool.end();
|
|
await closePool();
|
|
await closeRedisClient();
|
|
});
|
|
|
|
// ─── GET /marketplace ────────────────────────────────────────────────────────
|
|
|
|
describe('GET /api/v1/marketplace', () => {
|
|
it('should return 200 with a list of public agents', async () => {
|
|
const res = await request(app).get('/api/v1/marketplace');
|
|
|
|
expect(res.status).toBe(200);
|
|
const items: unknown[] = res.body.data ?? res.body;
|
|
expect(Array.isArray(items)).toBe(true);
|
|
});
|
|
|
|
it('should not expose private agents in the listing', async () => {
|
|
const res = await request(app).get('/api/v1/marketplace');
|
|
|
|
expect(res.status).toBe(200);
|
|
const items = (res.body.data ?? res.body) as Array<{ agentId: string }>;
|
|
const privateIds = items.map((a) => a.agentId);
|
|
expect(privateIds).not.toContain(PRIVATE_AGENT_ID);
|
|
});
|
|
|
|
it('should support pagination via ?page= and ?limit=', async () => {
|
|
const res = await request(app).get('/api/v1/marketplace?page=1&limit=5');
|
|
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
// ─── GET /marketplace/:id ────────────────────────────────────────────────────
|
|
|
|
describe('GET /api/v1/marketplace/:id', () => {
|
|
it('should return 200 with the public agent card', async () => {
|
|
const res = await request(app).get(`/api/v1/marketplace/${PUBLIC_AGENT_ID}`);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.agentId).toBe(PUBLIC_AGENT_ID);
|
|
});
|
|
|
|
it('should return 404 for a private agent', async () => {
|
|
const res = await request(app).get(`/api/v1/marketplace/${PRIVATE_AGENT_ID}`);
|
|
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it('should return 404 for a non-existent agent', async () => {
|
|
const res = await request(app).get(`/api/v1/marketplace/${uuidv4()}`);
|
|
|
|
expect(res.status).toBe(404);
|
|
});
|
|
});
|
|
});
|