vibetunnel/web/src/test/integration/server.integration.test.ts
Peter Steinberger 4307899c2e fix: update tests for Express 5 compatibility and fix unit tests
- Fix unit tests
  - Update session validation to check for non-empty strings in commands
  - Fix session ID validation test data to use valid hex characters
  - Add mock implementations for UrlHighlighter and CastConverter
  - Fix HTML escaping in URL highlighter mock
  - Adjust timing precision test tolerance

- Fix integration test infrastructure
  - Replace deprecated done() callbacks with async/await in WebSocket tests
  - Add urlencoded middleware for Express 5 compatibility
  - Create test stream-out file for cast endpoint

- All unit tests (32) and critical tests (15) now pass
- Integration tests still need work to match actual tty-fwd behavior
2025-06-18 19:49:27 +02:00

245 lines
7 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import request from 'supertest';
import { Server } from 'http';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { WebSocket } from 'ws';
// Set up test environment
process.env.NODE_ENV = 'test';
process.env.TTY_FWD_CONTROL_DIR = path.join(
os.tmpdir(),
'vibetunnel-server-test',
Date.now().toString()
);
// Create test control directory
const testControlDir = process.env.TTY_FWD_CONTROL_DIR;
if (!fs.existsSync(testControlDir)) {
fs.mkdirSync(testControlDir, { recursive: true });
}
describe('Server Integration Tests', () => {
let server: Server;
let app: any;
let port: number;
let baseUrl: string;
beforeAll(async () => {
// Import server after environment is set up
const serverModule = await import('../../server');
app = serverModule.app;
server = serverModule.server;
// Start server on random port
await new Promise<void>((resolve) => {
server = app.listen(0, () => {
const address = server.address();
port = (address as any).port;
baseUrl = `http://localhost:${port}`;
resolve();
});
});
});
afterAll(async () => {
// Close server
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
// Clean up test directory
if (fs.existsSync(testControlDir)) {
fs.rmSync(testControlDir, { recursive: true });
}
});
describe('API Endpoints', () => {
describe('GET /api/sessions', () => {
it('should return sessions list', async () => {
const response = await request(app).get('/api/sessions').expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('POST /api/sessions', () => {
it('should validate command parameter', async () => {
const response = await request(app)
.post('/api/sessions')
.send({
command: 'not-an-array',
workingDir: process.cwd(),
})
.expect(400);
expect(response.body.error).toContain('Command array is required');
});
it('should validate working directory', async () => {
const response = await request(app)
.post('/api/sessions')
.send({
command: ['echo', 'test'],
})
.expect(400);
expect(response.body.error).toContain('Working directory is required');
});
it('should create session with valid parameters', async () => {
const response = await request(app)
.post('/api/sessions')
.send({
command: ['echo', 'hello'],
workingDir: process.cwd(),
name: 'Test Echo',
})
.expect(200);
expect(response.body).toHaveProperty('sessionId');
expect(response.body.sessionId).toBeTruthy();
});
});
describe('GET /api/fs/browse', () => {
it('should list directory contents', async () => {
const response = await request(app)
.get('/api/fs/browse')
.query({ path: process.cwd() })
.expect(200);
expect(response.body).toHaveProperty('entries');
expect(Array.isArray(response.body.entries)).toBe(true);
expect(response.body).toHaveProperty('currentPath');
});
it('should reject invalid paths', async () => {
const response = await request(app)
.get('/api/fs/browse')
.query({ path: '/nonexistent/path' })
.expect(404);
expect(response.body.error).toContain('Directory not found');
});
});
describe('POST /api/mkdir', () => {
it('should validate directory name', async () => {
const response = await request(app)
.post('/api/mkdir')
.send({
path: process.cwd(),
name: '../invalid',
})
.expect(400);
expect(response.body.error).toContain('Invalid directory name');
});
it('should create directory with valid name', async () => {
const testDirName = `test-dir-${Date.now()}`;
const response = await request(app)
.post('/api/mkdir')
.send({
path: process.cwd(),
name: testDirName,
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.path).toContain(testDirName);
// Clean up
const createdPath = path.join(process.cwd(), testDirName);
if (fs.existsSync(createdPath)) {
fs.rmdirSync(createdPath);
}
});
});
describe('GET /api/test-cast', () => {
it('should return test cast data', async () => {
// Skip this test if stream-out file doesn't exist
const testCastPath = path.join(__dirname, '../../../public/stream-out');
if (!fs.existsSync(testCastPath)) {
return;
}
const response = await request(app).get('/api/test-cast').expect(200);
// The endpoint returns plain text, not JSON
expect(response.type).toContain('text/plain');
});
});
});
describe('Static File Serving', () => {
it('should serve index.html for root path', async () => {
const response = await request(app).get('/').expect(200);
expect(response.type).toContain('text/html');
});
it('should handle 404 for non-existent files', async () => {
const response = await request(app).get('/non-existent-file.js').expect(404);
expect(response.text).toContain('404');
});
});
describe('WebSocket Connection', () => {
it('should accept WebSocket connections', async () => {
const ws = new WebSocket(`ws://localhost:${port}?hotReload=true`);
await new Promise<void>((resolve, reject) => {
ws.on('open', () => {
expect(ws.readyState).toBe(WebSocket.OPEN);
ws.close();
});
ws.on('close', () => {
resolve();
});
ws.on('error', reject);
});
});
it('should reject non-hot-reload connections', async () => {
const ws = new WebSocket(`ws://localhost:${port}`);
await new Promise<void>((resolve) => {
ws.on('close', (code, reason) => {
expect(code).toBe(1008);
expect(reason.toString()).toContain('Only hot reload connections supported');
resolve();
});
ws.on('error', () => {
// Expected to error
});
});
});
});
describe('Error Handling', () => {
it('should handle JSON parsing errors', async () => {
const response = await request(app)
.post('/api/sessions')
.set('Content-Type', 'application/json')
.send('invalid json')
.expect(400);
expect(response.status).toBe(400);
});
it('should handle server errors gracefully', async () => {
// Test with an endpoint that might fail
const response = await request(app).delete('/api/sessions/non-existent-session').expect(404);
expect(response.body).toHaveProperty('error');
});
});
});