refactoring
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface IFixtureServer {
|
||||
start(port?: number): Promise<{ url: string; port: number }>;
|
||||
stop(): Promise<void>;
|
||||
getFixtureUrl(stepNumber: number): string;
|
||||
isRunning(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step number to fixture file mapping.
|
||||
* Steps 1-18 map to the corresponding HTML fixture files.
|
||||
*/
|
||||
const STEP_TO_FIXTURE: Record<number, string> = {
|
||||
1: '01-hosted-racing.html',
|
||||
2: '02-create-a-race.html',
|
||||
3: '03-race-information.html',
|
||||
4: '04-server-details.html',
|
||||
5: '05-set-admins.html',
|
||||
6: '06-add-an-admin.html',
|
||||
7: '07-time-limits.html',
|
||||
8: '08-set-cars.html',
|
||||
9: '09-add-a-car.html',
|
||||
10: '10-set-car-classes.html',
|
||||
11: '11-set-track.html',
|
||||
12: '12-add-a-track.html',
|
||||
13: '13-track-options.html',
|
||||
14: '14-time-of-day.html',
|
||||
15: '15-weather.html',
|
||||
16: '16-race-options.html',
|
||||
17: '17-team-driving.html',
|
||||
18: '18-track-conditions.html',
|
||||
};
|
||||
|
||||
export class FixtureServer implements IFixtureServer {
|
||||
private server: http.Server | null = null;
|
||||
private port: number = 3456;
|
||||
private fixturesPath: string;
|
||||
|
||||
constructor(fixturesPath?: string) {
|
||||
this.fixturesPath =
|
||||
fixturesPath ?? path.resolve(process.cwd(), 'html-dumps/iracing-hosted-sessions');
|
||||
}
|
||||
|
||||
async start(port: number = 3456): Promise<{ url: string; port: number }> {
|
||||
if (this.server) {
|
||||
return { url: `http://localhost:${this.port}`, port: this.port };
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
this.server.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
this.server = null;
|
||||
this.start(port + 1).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(this.port, () => {
|
||||
resolve({ url: `http://localhost:${this.port}`, port: this.port });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.close((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
this.server = null;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getFixtureUrl(stepNumber: number): string {
|
||||
const fixture = STEP_TO_FIXTURE[stepNumber];
|
||||
if (!fixture) {
|
||||
return `http://localhost:${this.port}/`;
|
||||
}
|
||||
return `http://localhost:${this.port}/${fixture}`;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.server !== null;
|
||||
}
|
||||
|
||||
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||
const urlPath = req.url || '/';
|
||||
|
||||
let fileName: string;
|
||||
if (urlPath === '/') {
|
||||
fileName = STEP_TO_FIXTURE[1];
|
||||
} else {
|
||||
fileName = urlPath.replace(/^\//, '');
|
||||
|
||||
const legacyMatch = fileName.match(/^step-(\d+)-/);
|
||||
if (legacyMatch) {
|
||||
const stepNum = Number(legacyMatch[1]);
|
||||
const mapped = STEP_TO_FIXTURE[stepNum];
|
||||
if (mapped) {
|
||||
fileName = mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = path.join(this.fixturesPath, fileName);
|
||||
|
||||
// Security check - prevent directory traversal
|
||||
if (!filePath.startsWith(this.fixturesPath)) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
const errno = (err as NodeJS.ErrnoException).code;
|
||||
if (errno === 'ENOENT') {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentTypes: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
};
|
||||
|
||||
const contentType = contentTypes[ext] || 'application/octet-stream';
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fixture filename for a given step number.
|
||||
*/
|
||||
export function getFixtureForStep(stepNumber: number): string | undefined {
|
||||
return STEP_TO_FIXTURE[stepNumber];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all step-to-fixture mappings.
|
||||
*/
|
||||
export function getAllStepFixtureMappings(): Record<number, string> {
|
||||
return { ...STEP_TO_FIXTURE };
|
||||
}
|
||||
Reference in New Issue
Block a user