agenthub/test/api-integration.test.ts
Paperclip FoundingEngineer bdd5d92ba7 Initial AgentHub codebase for Coolify deployment
Complete implementation ready for Coolify:
- Node.js 22 + Fastify + socket.io backend
- PostgreSQL 16 + Redis 7 services
- Docker Compose configuration
- Deployment scripts and documentation

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-01 21:25:57 +00:00

250 lines
7.3 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import type { FastifyInstance } from 'fastify';
import { buildApp } from '../src/app.js';
import { loadConfig } from '../src/config.js';
import { pool, closePool } from '../src/db/pool.js';
import { drizzle } from 'drizzle-orm/node-postgres';
import { agents, apiTokens, auditEvents } from '../src/db/schema.js';
import { sql } from 'drizzle-orm';
describe('J3 API Integration Tests', () => {
let app: FastifyInstance;
let config: ReturnType<typeof loadConfig>;
beforeAll(async () => {
config = loadConfig({
...process.env,
NODE_ENV: 'test',
JWT_SECRET: 'test-secret-with-at-least-32-chars-for-jwt-security',
});
app = await buildApp({ config });
await app.ready();
// Clean up test data
const db = drizzle(pool);
await db.delete(auditEvents);
await db.delete(apiTokens);
await db.delete(agents);
});
afterAll(async () => {
await app.close();
await closePool();
});
describe('Complete authentication flow', () => {
let agentId: string;
let apiToken: string;
let tokenId: string;
let _jwt: string;
it('should create an agent', async () => {
const response = await request(app.server)
.post('/api/v1/agents')
.send({
name: 'test-agent',
displayName: 'Test Agent',
role: 'agent',
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('test-agent');
expect(response.body.role).toBe('agent');
agentId = response.body.id;
// Verify audit event
const db = drizzle(pool);
const events = await db
.select()
.from(auditEvents)
.where(sql`${auditEvents.type} = 'agent-created'`);
expect(events.length).toBeGreaterThan(0);
});
it('should list agents', async () => {
const response = await request(app.server).get('/api/v1/agents').expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
expect(response.body.some((a: any) => a.id === agentId)).toBe(true);
});
it('should issue an API token', async () => {
const response = await request(app.server)
.post(`/api/v1/agents/${agentId}/tokens`)
.send({
scopes: { read: true, write: true },
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('secret');
expect(response.body.secret).toMatch(/^ah_live_[a-zA-Z0-9]{4}_/);
expect(response.body.agentId).toBe(agentId);
expect(response.body.status).toBe('active');
apiToken = response.body.secret;
tokenId = response.body.id;
// Verify audit event
const db = drizzle(pool);
const events = await db
.select()
.from(auditEvents)
.where(sql`${auditEvents.type} = 'token-issued'`);
expect(events.length).toBeGreaterThan(0);
});
it('should exchange API token for JWT', async () => {
const response = await request(app.server)
.post('/api/v1/sessions')
.send({
apiToken,
})
.expect(200);
expect(response.body).toHaveProperty('jwt');
expect(response.body.expiresIn).toBe(900); // 15 minutes
expect(response.body.agentId).toBe(agentId);
expect(response.body.agentName).toBe('test-agent');
expect(response.body.agentRole).toBe('agent');
_jwt = response.body.jwt;
// Verify audit event
const db = drizzle(pool);
const events = await db
.select()
.from(auditEvents)
.where(sql`${auditEvents.type} = 'jwt-issued'`);
expect(events.length).toBeGreaterThan(0);
});
it('should reject invalid API token', async () => {
await request(app.server)
.post('/api/v1/sessions')
.send({
apiToken: 'ah_live_XXXX_invalid',
})
.expect(401);
});
it('should revoke API token', async () => {
await request(app.server).delete(`/api/v1/tokens/${tokenId}`).expect(204);
// Verify audit event
const db = drizzle(pool);
const events = await db
.select()
.from(auditEvents)
.where(sql`${auditEvents.type} = 'token-revoked'`);
expect(events.length).toBeGreaterThan(0);
});
it('should reject revoked token', async () => {
await request(app.server)
.post('/api/v1/sessions')
.send({
apiToken,
})
.expect(401);
});
});
describe('Token rotation scenario', () => {
let agentId: string;
let oldToken: string;
let oldTokenId: string;
let newToken: string;
it('should create agent for rotation test', async () => {
const response = await request(app.server)
.post('/api/v1/agents')
.send({
name: 'rotation-agent',
displayName: 'Rotation Agent',
role: 'agent',
})
.expect(201);
agentId = response.body.id;
});
it('should issue first token', async () => {
const response = await request(app.server)
.post(`/api/v1/agents/${agentId}/tokens`)
.send({})
.expect(201);
oldToken = response.body.secret;
oldTokenId = response.body.id;
});
it('old token should work before rotation', async () => {
await request(app.server).post('/api/v1/sessions').send({ apiToken: oldToken }).expect(200);
});
it('should issue new token (simulating rotation)', async () => {
const response = await request(app.server)
.post(`/api/v1/agents/${agentId}/tokens`)
.send({})
.expect(201);
newToken = response.body.secret;
});
it('both tokens should work during overlap period', async () => {
// Old token still valid
await request(app.server).post('/api/v1/sessions').send({ apiToken: oldToken }).expect(200);
// New token also valid
await request(app.server).post('/api/v1/sessions').send({ apiToken: newToken }).expect(200);
});
it('should revoke old token explicitly', async () => {
await request(app.server).delete(`/api/v1/tokens/${oldTokenId}`).expect(204);
});
it('old token should fail after revocation', async () => {
await request(app.server).post('/api/v1/sessions').send({ apiToken: oldToken }).expect(401);
});
it('new token should still work after old token revoked', async () => {
await request(app.server).post('/api/v1/sessions').send({ apiToken: newToken }).expect(200);
});
});
describe('Validation tests', () => {
it('should reject invalid agent name', async () => {
await request(app.server)
.post('/api/v1/agents')
.send({
name: 'Invalid Name!',
displayName: 'Test',
role: 'agent',
})
.expect(400);
});
it('should reject invalid role', async () => {
await request(app.server)
.post('/api/v1/agents')
.send({
name: 'test',
displayName: 'Test',
role: 'invalid',
})
.expect(400);
});
it('should reject token creation for non-existent agent', async () => {
await request(app.server)
.post('/api/v1/agents/00000000-0000-0000-0000-000000000000/tokens')
.send({})
.expect(404);
});
});
});