diff --git a/src/controllers/SpeedTestController.ts b/src/controllers/SpeedTestController.ts index cbc0f63..59993d7 100644 --- a/src/controllers/SpeedTestController.ts +++ b/src/controllers/SpeedTestController.ts @@ -1,7 +1,7 @@ import type {Server as BunServer, BunRequest} from 'bun'; import type {BunMethodRoutes} from '../net/http/BunHttpServer'; import {LoggerFactory} from '@techniker-me/logger'; -import type { ISpeedTestService } from '../interfaces/ISpeedTest'; +import type { ISpeedTestService } from '../core/interfaces/ISpeedTestService'; export class SpeedTestController { private readonly _logger = LoggerFactory.getLogger('SpeedTestController'); @@ -39,7 +39,7 @@ export class SpeedTestController { try { const receivedData = new Uint8Array(await request.arrayBuffer()); - const result = await this._service.validateUpload(requestId, receivedData); + const result = await this._service.validateTest(requestId, receivedData); return new Response(JSON.stringify(result), { status: result.valid ? 200 : 400, @@ -50,10 +50,9 @@ export class SpeedTestController { this._logger.error('Error processing speedtest data: [%s]', error); // Distinguish between user error (404, 400) and server error (500) - // Simplification: if message is known (from service), return 400 or 404. let status = 500; if (error.message === 'Request not found or expired') status = 404; - if (error.message.startsWith('Data size mismatch')) status = 400; + if (error.message?.startsWith('Data size mismatch')) status = 400; return new Response(JSON.stringify({error: error.message || 'Failed to process request'}), {status, headers: {'Content-Type': 'application/json'}}); } @@ -62,25 +61,29 @@ export class SpeedTestController { private async getData(request: BunRequest, _server: BunServer): Promise { const url = new URL(request.url); const size = parseInt(url.searchParams.get('size') ?? this.defaultSize.toString()); - const timeout = url.searchParams.get('timeout') ? parseInt(url.searchParams.get('timeout')!) : undefined; + const timeout = url.searchParams.get('timeout'); + + const options: Record = {}; + if (timeout) options['timeout'] = timeout; + if (url.searchParams.get('hashAlgorithm')) options['hashAlgorithm'] = url.searchParams.get('hashAlgorithm')!; + if (url.searchParams.get('hashEncoding')) options['hashEncoding'] = url.searchParams.get('hashEncoding')!; + if (url.searchParams.get('chunkSize')) options['chunkSize'] = url.searchParams.get('chunkSize')!; try { - const { session, data } = await this._service.generateTestData(size, { timeout }); - + const result = await this._service.initiateTest(size, options); + + // Convert DataView to Uint8Array for Response + const dataArray = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength); + const headers = new Headers(); - headers.set('Content-Type', 'application/octet-stream'); - headers.set('Content-Length', session.size.toString()); - headers.set('Content-Hash', session.hash); - headers.set('Content-Hash-Algorithm', session.hashAlgorithm); - headers.set('Content-Hash-Encoding', session.hashEncoding); - headers.set('Content-Chunk-Size', session.chunkSize.toString()); - headers.set('X-Request-ID', session.requestId); + Object.entries(result.headers).forEach(([key, value]) => { + headers.set(key, value); + }); - return new Response(data, {status: 200, headers}); + return new Response(dataArray, {status: 200, headers}); } catch (error: any) { this._logger.error('Error generating data: [%s]', error); return new Response(JSON.stringify({error: 'Failed to generate data'}), {status: 500, headers: {'Content-Type': 'application/json'}}); } } } - diff --git a/src/data/RandomData.ts b/src/data/RandomData.ts new file mode 100644 index 0000000..3c567ae --- /dev/null +++ b/src/data/RandomData.ts @@ -0,0 +1,34 @@ +import {LoggerFactory} from '@techniker-me/logger'; +import {getRandomValues, createHash} from 'node:crypto'; + +export class RandomData { + private readonly _logger = LoggerFactory.getLogger('RandomData'); + private readonly _size: number; + private readonly _data: Uint8Array; + private readonly _dataHashAlgorithm: string; + private _dataHash: string; + + constructor(size: number) { + this._size = size; + this._data = getRandomValues(new Uint8Array(size)); + this._dataHashAlgorithm = 'sha256'; + this._dataHash = this.generateDataHash(this._data); + } + + get hash(): string { + return this._dataHash; + } + + get data(): DataView { + return new DataView(this._data.buffer); + } + + get dataByteLength(): number { + return this._data.byteLength; + } + + private generateDataHash(data: Uint8Array): string { + return createHash(this._dataHashAlgorithm).update(data).digest('base64'); + } +} + diff --git a/src/index.ts b/src/index.ts index 3e51c43..e43a70e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,17 @@ import HttpServerFactory from './net/http/HttpServerFactory'; import {HealthCheckApiRoutes} from './health/HealthCheckRoute'; import HealthCheck from './health/HealthCheck'; -import {SpeedTestApiRoutes} from './api/SpeedTestApiRoutes'; +import {SpeedTestController} from './controllers/SpeedTestController'; +import {SpeedTestService} from './services/SpeedTestService'; +import {InMemoryRequestRepository} from './repositories/InMemoryRequestRepository'; const healthCheck = new HealthCheck(); const healthCheckRoutes = new HealthCheckApiRoutes(healthCheck); -const speedTestApiRoutes = new SpeedTestApiRoutes(); + +// Dependency Injection Composition Root +const speedTestRepository = new InMemoryRequestRepository(); +const speedTestService = new SpeedTestService(speedTestRepository); +const speedTestController = new SpeedTestController(speedTestService); // Frontend serving function - using Bun's super simple static file serving const serveFrontend = async (_request: any, _server: any) => new Response(Bun.file('./frontend.html'), { @@ -18,7 +24,7 @@ const serveFrontend = async (_request: any, _server: any) => new Response(Bun.fi const server = HttpServerFactory.createBunHttpServer({ '/': serveFrontend, ...healthCheckRoutes.getGETRoute(), - ...speedTestApiRoutes.getRoutes(), + ...speedTestController.getRoutes(), }); const listenPromise = server.listen(8080); diff --git a/src/interfaces/ISpeedTestRepository.ts b/src/interfaces/ISpeedTestRepository.ts index ce8169e..735864a 100644 --- a/src/interfaces/ISpeedTestRepository.ts +++ b/src/interfaces/ISpeedTestRepository.ts @@ -1,4 +1,4 @@ -import { SpeedTestSession } from '../models/SpeedTestSession'; +import type { SpeedTestSession } from '../models/SpeedTestSession'; export interface ISpeedTestRepository { save(session: SpeedTestSession): void; diff --git a/src/interfaces/ISpeedTestService.ts b/src/interfaces/ISpeedTestService.ts index 29b1cb0..20b6b1e 100644 --- a/src/interfaces/ISpeedTestService.ts +++ b/src/interfaces/ISpeedTestService.ts @@ -1,4 +1,4 @@ -import { SpeedTestResult } from '../models/SpeedTestResult'; +import type { SpeedTestResult } from '../models/SpeedTestResult'; export interface SpeedTestServiceInitParams { size: number;