166 lines
4.2 KiB
TypeScript
166 lines
4.2 KiB
TypeScript
import * as http from 'http';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
export interface IFixtureServerService {
|
|
start(port: number, fixturesPath: string): Promise<void>;
|
|
stop(): Promise<void>;
|
|
waitForReady(timeoutMs: number): Promise<boolean>;
|
|
getBaseUrl(): string;
|
|
isRunning(): boolean;
|
|
}
|
|
|
|
export class FixtureServerService implements IFixtureServerService {
|
|
private server: http.Server | null = null;
|
|
private port: number = 3456;
|
|
private resolvedFixturesPath: string = '';
|
|
|
|
async start(port: number, fixturesPath: string): Promise<void> {
|
|
if (this.server) {
|
|
throw new Error('Fixture server is already running');
|
|
}
|
|
|
|
this.port = port;
|
|
this.resolvedFixturesPath = path.resolve(fixturesPath);
|
|
|
|
if (!fs.existsSync(this.resolvedFixturesPath)) {
|
|
throw new Error(`Fixtures path does not exist: ${this.resolvedFixturesPath}`);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.server = http.createServer((req, res) => {
|
|
this.handleRequest(req, res);
|
|
});
|
|
|
|
this.server.on('error', (error) => {
|
|
this.server = null;
|
|
reject(error);
|
|
});
|
|
|
|
this.server.listen(this.port, () => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
if (!this.server) {
|
|
return;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.server!.close((error) => {
|
|
this.server = null;
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async waitForReady(timeoutMs: number = 5000): Promise<boolean> {
|
|
const startTime = Date.now();
|
|
const pollInterval = 100;
|
|
|
|
while (Date.now() - startTime < timeoutMs) {
|
|
const isReady = await this.checkHealth();
|
|
if (isReady) {
|
|
return true;
|
|
}
|
|
await this.sleep(pollInterval);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
getBaseUrl(): string {
|
|
return `http://localhost:${this.port}`;
|
|
}
|
|
|
|
isRunning(): boolean {
|
|
return this.server !== null;
|
|
}
|
|
|
|
private async checkHealth(): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const req = http.get(`${this.getBaseUrl()}/health`, (res) => {
|
|
resolve(res.statusCode === 200);
|
|
});
|
|
|
|
req.on('error', () => {
|
|
resolve(false);
|
|
});
|
|
|
|
req.setTimeout(1000, () => {
|
|
req.destroy();
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
private sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
const url = req.url || '/';
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
if (url === '/health') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ status: 'ok' }));
|
|
return;
|
|
}
|
|
|
|
const sanitizedUrl = url.startsWith('/') ? url.slice(1) : url;
|
|
const filePath = path.join(this.resolvedFixturesPath, sanitizedUrl || 'index.html');
|
|
|
|
const normalizedFilePath = path.normalize(filePath);
|
|
if (!normalizedFilePath.startsWith(this.resolvedFixturesPath)) {
|
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
fs.stat(normalizedFilePath, (statErr, stats) => {
|
|
if (statErr || !stats.isFile()) {
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
const ext = path.extname(normalizedFilePath).toLowerCase();
|
|
const contentType = this.getContentType(ext);
|
|
|
|
fs.readFile(normalizedFilePath, (readErr, data) => {
|
|
if (readErr) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
res.end(data);
|
|
});
|
|
});
|
|
}
|
|
|
|
private getContentType(ext: string): string {
|
|
const mimeTypes: Record<string, string> = {
|
|
'.html': 'text/html',
|
|
'.css': 'text/css',
|
|
'.js': 'application/javascript',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
};
|
|
|
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
}
|
|
} |