LCOV - code coverage report
Current view: top level - dashql-app/src/connections/hyper - hyperdb_http_client_mock.ts Coverage Total Hit
Test: DashQL 915828b Lines: 99.3 % 143 142
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import { HttpClient, HttpFetchResult } from '../../platform/http/http_client.js';
       2              : import {
       3              :     ColumnDefinition,
       4              :     CompletionStatus,
       5              :     ExecuteQueryRequest,
       6              :     ExecutionStats,
       7              :     HyperDatabaseHttpClient,
       8              :     HyperHttpAuthProvider,
       9              :     QueryErrorResponse,
      10              :     QueryStatus,
      11              : } from './hyperdb_http_client.js';
      12              : import { TestLogger } from '../../platform/logger/test_logger.js';
      13              : 
      14            1 : const ARROW_CONTENT_TYPE = "application/vnd.apache.arrow.stream";
      15              : 
      16              : // ---------------------------------------------------------------------------
      17              : // Helpers
      18              : // ---------------------------------------------------------------------------
      19              : 
      20            1 : function makeArrowResult(status: number, bytes: Uint8Array, extraHeaders?: Record<string, string>): HttpFetchResult {
      21            1 :     const headers = new Headers({ 'Content-Type': ARROW_CONTENT_TYPE, ...extraHeaders });
      22            1 :     return {
      23            1 :         headers,
      24            1 :         status,
      25            1 :         statusText: status === 200 ? 'OK' : String(status),
      26            1 :         json: async () => { throw new Error('response is Arrow, not JSON'); },
      27            1 :         text: async () => { throw new Error('response is Arrow, not text'); },
      28            1 :         arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer,
      29            1 :     };
      30            1 : }
      31              : 
      32            1 : function makeErrorResult(status: number, body: QueryErrorResponse): HttpFetchResult {
      33            1 :     const text = JSON.stringify(body);
      34            1 :     const headers = new Headers({ 'Content-Type': 'application/json' });
      35            1 :     return {
      36            1 :         headers,
      37            1 :         status,
      38            1 :         statusText: String(status),
      39            1 :         json: async () => JSON.parse(text),
      40            1 :         text: async () => text,
      41            1 :         arrayBuffer: async () => new TextEncoder().encode(text).buffer,
      42            1 :     };
      43            1 : }
      44              : 
      45            1 : function statusHeader(qs: QueryStatus): Record<string, string> {
      46            1 :     return { 'x-hyperdb-status': JSON.stringify(qs) };
      47            1 : }
      48              : 
      49              : // ---------------------------------------------------------------------------
      50              : // Programmable request handler
      51              : // ---------------------------------------------------------------------------
      52              : 
      53              : export type MockQueryHandler = (sql: string, request: ExecuteQueryRequest) => MockQueryResult;
      54              : 
      55              : export interface MockQueryResult {
      56              :     queryId?: string;
      57              :     completionStatus?: CompletionStatus;
      58              :     columns?: ColumnDefinition[];
      59              :     arrowBytes?: Uint8Array;
      60              :     errorStatus?: number;
      61              :     errorResponse?: QueryErrorResponse;
      62              : }
      63              : 
      64              : // ---------------------------------------------------------------------------
      65              : // Stored query state
      66              : // ---------------------------------------------------------------------------
      67              : 
      68              : export interface MockPendingQuery {
      69              :     queryId: string;
      70              :     completionStatus: CompletionStatus;
      71              :     columns: ColumnDefinition[];
      72              :     arrowBytes: Uint8Array;
      73              :     chunkCount: number;
      74              :     rowCount: number;
      75              :     expirationTime: string;
      76              :     executionStats: ExecutionStats;
      77              : }
      78              : 
      79              : // ---------------------------------------------------------------------------
      80              : // Mock HttpClient
      81              : // ---------------------------------------------------------------------------
      82              : 
      83            1 : export class HyperHttpClientMock implements HttpClient {
      84            1 :     private queries = new Map<string, MockPendingQuery>();
      85            1 :     private handler: MockQueryHandler | null = null;
      86            1 :     private nextQueryId = 1;
      87              : 
      88            1 :     setHandler(handler: MockQueryHandler): void {
      89            1 :         this.handler = handler;
      90            1 :     }
      91              : 
      92            1 :     registerQuery(query: MockPendingQuery): void {
      93            1 :         this.queries.set(query.queryId, query);
      94            1 :     }
      95              : 
      96            1 :     private newQueryId(): string {
      97            1 :         return `mock-query-${this.nextQueryId++}`;
      98            1 :     }
      99              : 
     100            1 :     private buildQueryStatus(q: MockPendingQuery): QueryStatus {
     101            1 :         return {
     102            1 :             queryId: q.queryId,
     103            1 :             completionStatus: q.completionStatus,
     104            1 :             chunkCount: q.chunkCount,
     105            1 :             rowCount: q.rowCount,
     106            1 :             progress: q.completionStatus === 'RUNNING_OR_UNSPECIFIED' ? 0 : 100,
     107            1 :             expirationTime: q.expirationTime,
     108            1 :             executionStats: q.executionStats,
     109            1 :         };
     110            1 :     }
     111              : 
     112            1 :     async fetch(input: URL | Request | string, init?: RequestInit): Promise<HttpFetchResult> {
     113            1 :         const url = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input.url);
     114            1 :         const method = (init?.method ?? 'GET').toUpperCase();
     115            1 :         const path = url.pathname;
     116              : 
     117            1 :         if (method === 'POST' && path === '/api/v3/query') {
     118            1 :             return this.handleExecuteQuery(init);
     119              :         }
     120              : 
     121            1 :         const statusMatch = path.match(/^\/api\/v3\/query\/([^/]+)$/);
     122            1 :         if (statusMatch) {
     123            1 :             if (method === 'GET') return this.handleGetStatus(decodeURIComponent(statusMatch[1]));
     124            1 :             if (method === 'DELETE') return this.handleCancelQuery(decodeURIComponent(statusMatch[1]));
     125            1 :         }
     126              : 
     127            1 :         const chunkMatch = path.match(/^\/api\/v3\/query\/([^/]+)\/chunk\/(\d+)$/);
     128            1 :         if (method === 'GET' && chunkMatch) {
     129            1 :             return this.handleGetChunk(decodeURIComponent(chunkMatch[1]), parseInt(chunkMatch[2], 10));
     130              :         }
     131              : 
     132            1 :         const rowMatch = path.match(/^\/api\/v3\/query\/([^/]+)\/row$/);
     133            1 :         if (method === 'GET' && rowMatch) {
     134            1 :             return this.handleGetRows(decodeURIComponent(rowMatch[1]), url);
     135              :         }
     136              : 
     137            0 :         return makeErrorResult(404, { error: 'NOT_FOUND', message: `No mock for ${method} ${path}` });
     138            1 :     }
     139              : 
     140            1 :     private handleExecuteQuery(init?: RequestInit): HttpFetchResult {
     141            1 :         const request: ExecuteQueryRequest = init?.body ? JSON.parse(init.body as string) : { sql: '' };
     142              : 
     143            1 :         let result: MockQueryResult = { completionStatus: 'RESULTS_PRODUCED' };
     144            1 :         if (this.handler) {
     145            1 :             result = this.handler(request.sql, request);
     146              :         }
     147              : 
     148            1 :         if (result.errorStatus !== undefined) {
     149            1 :             return makeErrorResult(result.errorStatus, result.errorResponse ?? { error: 'HY000', message: 'mock error' });
     150              :         }
     151              : 
     152            1 :         const queryId = result.queryId ?? this.newQueryId();
     153            1 :         const completionStatus = result.completionStatus ?? 'RESULTS_PRODUCED';
     154            1 :         const arrowBytes = result.arrowBytes ?? new Uint8Array(0);
     155              : 
     156            1 :         const pending: MockPendingQuery = {
     157            1 :             queryId,
     158            1 :             completionStatus,
     159            1 :             columns: result.columns ?? [],
     160            1 :             arrowBytes,
     161            1 :             chunkCount: completionStatus === 'RUNNING_OR_UNSPECIFIED' ? 0 : 1,
     162            1 :             rowCount: 0,
     163            1 :             expirationTime: new Date(Date.now() + 300_000).toISOString(),
     164            1 :             executionStats: { wallClockTime: 1, rowsProcessed: 0 },
     165            1 :         };
     166            1 :         this.queries.set(queryId, pending);
     167              : 
     168            1 :         const qs = this.buildQueryStatus(pending);
     169            1 :         return makeArrowResult(200, arrowBytes, statusHeader(qs));
     170            1 :     }
     171              : 
     172            1 :     private handleGetStatus(queryId: string): HttpFetchResult {
     173            1 :         const q = this.queries.get(queryId);
     174            1 :         if (!q) {
     175            1 :             return makeErrorResult(404, { error: 'NOT_FOUND', message: `Query ${queryId} not found` });
     176              :         }
     177              :         // Status endpoint returns JSON (not Arrow)
     178            1 :         const text = JSON.stringify(this.buildQueryStatus(q));
     179            1 :         return {
     180            1 :             headers: new Headers({ 'Content-Type': 'application/json' }),
     181            1 :             status: 200,
     182            1 :             statusText: 'OK',
     183            1 :             json: async () => JSON.parse(text),
     184            1 :             text: async () => text,
     185            1 :             arrayBuffer: async () => new TextEncoder().encode(text).buffer,
     186            1 :         };
     187            1 :     }
     188              : 
     189            1 :     private handleCancelQuery(queryId: string): HttpFetchResult {
     190            1 :         this.queries.delete(queryId);
     191            1 :         return {
     192            1 :             headers: new Headers(),
     193            1 :             status: 204,
     194            1 :             statusText: 'No Content',
     195            1 :             json: async () => null,
     196            1 :             text: async () => '',
     197            1 :             arrayBuffer: async () => new ArrayBuffer(0),
     198            1 :         };
     199            1 :     }
     200              : 
     201            1 :     private handleGetChunk(queryId: string, _chunkId: number): HttpFetchResult {
     202            1 :         const q = this.queries.get(queryId);
     203            1 :         if (!q) {
     204            1 :             return makeErrorResult(404, { error: 'NOT_FOUND', message: `Query ${queryId} not found` });
     205              :         }
     206            1 :         const qs = this.buildQueryStatus(q);
     207            1 :         return makeArrowResult(200, q.arrowBytes, statusHeader(qs));
     208            1 :     }
     209              : 
     210            1 :     private handleGetRows(queryId: string, url: URL): HttpFetchResult {
     211            1 :         const q = this.queries.get(queryId);
     212            1 :         if (!q) {
     213            1 :             return makeErrorResult(404, { error: 'NOT_FOUND', message: `Query ${queryId} not found` });
     214              :         }
     215              :         // offset/limit/byteLimit are query params the real server uses; the mock
     216              :         // returns the full arrowBytes slice since Arrow streaming handles framing.
     217            1 :         return makeArrowResult(200, q.arrowBytes, {});
     218            1 :     }
     219            1 : }
     220              : 
     221              : // ---------------------------------------------------------------------------
     222              : // Factory
     223              : // ---------------------------------------------------------------------------
     224              : 
     225            1 : const MOCK_BASE_URL = new URL('https://mock.hyperdb.local');
     226              : 
     227            1 : const MOCK_AUTH: HyperHttpAuthProvider = {
     228            1 :     getAuthHeaders: async () => ({ Authorization: 'Bearer mock-token' }),
     229            1 : };
     230              : 
     231            1 : export function createMockHyperHttpClient(httpMock?: HyperHttpClientMock): {
     232              :     client: HyperDatabaseHttpClient;
     233              :     mock: HyperHttpClientMock;
     234            1 : } {
     235            1 :     const mock = httpMock ?? new HyperHttpClientMock();
     236            1 :     const client = new HyperDatabaseHttpClient(mock, MOCK_BASE_URL, MOCK_AUTH, new TestLogger());
     237            1 :     return { client, mock };
     238            1 : }
        

Generated by: LCOV version 2.0-1