api-routes.test.js 5.6 KB
/**
 * Integration Tests — API Routes
 *
 * Spins up Express with arena routes and tests key endpoints.
 * Uses temp SQLite DB to avoid side effects.
 */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import express from 'express';
import http from 'http';
import fs from 'fs';

const TEST_DB_PATH = '/tmp/test-integration-skills.sqlite';
const TEST_SKILLS_DIR = '/tmp/test-int-skills';
const TEST_DATA_DIR = '/tmp/test-int-data';

let app, server, baseUrl;

function request(method, urlStr, body = null) {
  return new Promise((resolve, reject) => {
    const url = new URL(urlStr, baseUrl);
    const opts = { method, hostname: url.hostname, port: url.port, path: url.pathname + url.search, headers: { 'Content-Type': 'application/json' } };
    const req = http.request(opts, (res) => {
      let data = '';
      res.on('data', (d) => data += d);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
        catch { resolve({ status: res.statusCode, data }); }
      });
    });
    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

describe('API Integration Tests', () => {
  beforeAll(async () => {
    [TEST_DB_PATH, TEST_DB_PATH + '-wal', TEST_DB_PATH + '-shm'].forEach(f => { try { fs.unlinkSync(f); } catch {} });
    fs.mkdirSync(TEST_SKILLS_DIR, { recursive: true });
    fs.mkdirSync(TEST_DATA_DIR, { recursive: true });

    process.env.SKILLS_DB_PATH = TEST_DB_PATH;
    process.env.BENCHMARK_DB_PATH = '/tmp/test-integration-bench.sqlite';
    process.env.PORT = '0';
    process.env.SKILLS_CODE_DIR = TEST_SKILLS_DIR;
    process.env.SKILLS_DATA_DIR = TEST_DATA_DIR;
    process.env.JWT_SECRET = 'test-secret-key-for-integration';
    process.env.API_KEYS = 'test-api-key-123';

    // Clear module cache for clean state
    Object.keys(require.cache).filter(k =>
      k.includes('skill-db') || k.includes('benchmark-db') || k.includes('/config')
    ).forEach(k => delete require.cache[k]);

    app = express();
    app.use(express.json());

    try {
      const arenaRoutes = require('../../routes/arena');
      app.use('/api/arena', arenaRoutes);
    } catch (e) { console.warn('Skipping arena routes:', e.message); }

    server = http.createServer(app);
    await new Promise((resolve) => {
      server.listen(0, () => {
        baseUrl = `http://127.0.0.1:${server.address().port}`;
        resolve();
      });
    });
  });

  afterAll(async () => {
    if (server) await new Promise(r => server.close(r));
    [TEST_DB_PATH, TEST_DB_PATH + '-wal', TEST_DB_PATH + '-shm',
     '/tmp/test-integration-bench.sqlite'].forEach(f => { try { fs.unlinkSync(f); } catch {} });
    try { fs.rmSync(TEST_SKILLS_DIR, { recursive: true }); } catch {}
    try { fs.rmSync(TEST_DATA_DIR, { recursive: true }); } catch {}
  });

  describe('Arena Routes', () => {
    it('GET /api/arena/db-stats should return stats', async () => {
      const res = await request('GET', '/api/arena/db-stats');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('total');
    });

    it('GET /api/arena/leaderboard should return paginated results', async () => {
      const res = await request('GET', '/api/arena/leaderboard?limit=10&page=1');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('skills');
      expect(res.data).toHaveProperty('total');
      expect(Array.isArray(res.data.skills)).toBe(true);
    });

    it('GET /api/arena/taxonomy should return taxonomy data', async () => {
      const res = await request('GET', '/api/arena/taxonomy');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('roles');
      expect(res.data).toHaveProperty('scenes');
      expect(res.data).toHaveProperty('platforms');
    });

    it('GET /api/arena/stats should return arena stats', async () => {
      const res = await request('GET', '/api/arena/stats');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('totalSkills');
    });

    it('GET /api/arena/scenes should return scene listing', async () => {
      const res = await request('GET', '/api/arena/scenes');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('scenes');
    });

    it('GET /api/arena/skill/nonexistent returns 404', async () => {
      const res = await request('GET', '/api/arena/skill/definitely_not_a_real_skill_xyz');
      expect(res.status).toBe(404);
    });

    it('POST /api/arena/submit with valid data should register a skill', async () => {
      const res = await request('POST', '/api/arena/submit', {
        skillName: 'integration_test_skill', description: 'Test skill for integration',
        domain: 'test', scope: 'unit',
      });
      // Expect 200 (created) or 409 (already exists)
      expect([200, 201, 409]).toContain(res.status);
    });

    it('GET /api/arena/leaderboard returns non-empty list', async () => {
      const res = await request('GET', '/api/arena/leaderboard?limit=5');
      expect(res.status).toBe(200);
      expect(res.data.skills.length).toBeGreaterThanOrEqual(0);
    });

    it('GET /api/arena/skill/:name/favorite query works', async () => {
      const res = await request('GET', '/api/arena/skill/integration_test_skill/favorite?userId=test-user-1');
      expect(res.status).toBe(200);
      expect(res.data).toHaveProperty('favorited');
      expect(res.data).toHaveProperty('count');
    });

    it('POST /api/arena/skill/:name/favorite should toggle favorite', async () => {
      const res = await request('POST', '/api/arena/skill/integration_test_skill/favorite', { userId: 'test-user-1' });
      expect(res.status).toBe(200);
    });
  });
});