diff --git a/lib/DBSQLClient.ts b/lib/DBSQLClient.ts index de9d9114..d3d9427c 100644 --- a/lib/DBSQLClient.ts +++ b/lib/DBSQLClient.ts @@ -7,6 +7,7 @@ import { TProtocolVersion } from '../thrift/TCLIService_types'; import IDBSQLClient, { ClientOptions, ConnectionOptions, OpenSessionRequest } from './contracts/IDBSQLClient'; import IDriver from './contracts/IDriver'; import IClientContext, { ClientConfig } from './contracts/IClientContext'; +import IThriftClient from './contracts/IThriftClient'; import HiveDriver from './hive/HiveDriver'; import DBSQLSession from './DBSQLSession'; import IDBSQLSession from './contracts/IDBSQLSession'; @@ -43,6 +44,8 @@ function getInitialNamespaceOptions(catalogName?: string, schemaName?: string) { }; } +export type ThriftLibrary = Pick; + export default class DBSQLClient extends EventEmitter implements IDBSQLClient, IClientContext { private static defaultLogger?: IDBSQLLogger; @@ -52,7 +55,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I private authProvider?: IAuthentication; - private client?: TCLIService.Client; + private client?: IThriftClient; private readonly driver = new HiveDriver({ context: this, @@ -60,9 +63,9 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I private readonly logger: IDBSQLLogger; - private readonly thrift = thrift; + private thrift: ThriftLibrary = thrift; - private sessions = new CloseableCollection(); + private readonly sessions = new CloseableCollection(); private static getDefaultLogger(): IDBSQLLogger { if (!this.defaultLogger) { @@ -113,7 +116,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I }; } - private initAuthProvider(options: ConnectionOptions, authProvider?: IAuthentication): IAuthentication { + private createAuthProvider(options: ConnectionOptions, authProvider?: IAuthentication): IAuthentication { if (authProvider) { return authProvider; } @@ -143,6 +146,10 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I } } + private createConnectionProvider(options: ConnectionOptions): IConnectionProvider { + return new HttpConnection(this.getConnectionOptions(options), this); + } + /** * Connects DBSQLClient to endpoint * @public @@ -153,9 +160,9 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I * const session = client.connect({host, path, token}); */ public async connect(options: ConnectionOptions, authProvider?: IAuthentication): Promise { - this.authProvider = this.initAuthProvider(options, authProvider); + this.authProvider = this.createAuthProvider(options, authProvider); - this.connectionProvider = new HttpConnection(this.getConnectionOptions(options), this); + this.connectionProvider = this.createConnectionProvider(options); const thriftConnection = await this.connectionProvider.getThriftConnection(); @@ -238,7 +245,7 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I return this.connectionProvider; } - public async getClient(): Promise { + public async getClient(): Promise { const connectionProvider = await this.getConnectionProvider(); if (!this.client) { diff --git a/lib/DBSQLOperation.ts b/lib/DBSQLOperation.ts index 84bde915..e7ab4bb6 100644 --- a/lib/DBSQLOperation.ts +++ b/lib/DBSQLOperation.ts @@ -64,7 +64,7 @@ export default class DBSQLOperation implements IOperation { private metadata?: TGetResultSetMetadataResp; - private state: number = TOperationState.INITIALIZED_STATE; + private state: TOperationState = TOperationState.INITIALIZED_STATE; // Once operation is finished or fails - cache status response, because subsequent calls // to `getOperationStatus()` may fail with irrelevant errors, e.g. HTTP 404 diff --git a/lib/connection/auth/DatabricksOAuth/AuthorizationCode.ts b/lib/connection/auth/DatabricksOAuth/AuthorizationCode.ts index f6973a4d..f2263bcb 100644 --- a/lib/connection/auth/DatabricksOAuth/AuthorizationCode.ts +++ b/lib/connection/auth/DatabricksOAuth/AuthorizationCode.ts @@ -6,50 +6,19 @@ import { OAuthScopes, scopeDelimiter } from './OAuthScope'; import IClientContext from '../../../contracts/IClientContext'; import AuthenticationError from '../../../errors/AuthenticationError'; +export type DefaultOpenAuthUrlCallback = (authUrl: string) => Promise; + +export type OpenAuthUrlCallback = (authUrl: string, defaultOpenAuthUrl: DefaultOpenAuthUrlCallback) => Promise; + export interface AuthorizationCodeOptions { client: BaseClient; ports: Array; context: IClientContext; + openAuthUrl?: OpenAuthUrlCallback; } -async function startServer( - host: string, - port: number, - requestHandler: (req: IncomingMessage, res: ServerResponse) => void, -): Promise { - const server = http.createServer(requestHandler); - - return new Promise((resolve, reject) => { - const errorListener = (error: Error) => { - server.off('error', errorListener); - reject(error); - }; - - server.on('error', errorListener); - server.listen(port, host, () => { - server.off('error', errorListener); - resolve(server); - }); - }); -} - -async function stopServer(server: Server): Promise { - if (!server.listening) { - return; - } - - return new Promise((resolve, reject) => { - const errorListener = (error: Error) => { - server.off('error', errorListener); - reject(error); - }; - - server.on('error', errorListener); - server.close(() => { - server.off('error', errorListener); - resolve(); - }); - }); +async function defaultOpenAuthUrl(authUrl: string): Promise { + await open(authUrl); } export interface AuthorizationCodeFetchResult { @@ -65,16 +34,12 @@ export default class AuthorizationCode { private readonly host: string = 'localhost'; - private readonly ports: Array; + private readonly options: AuthorizationCodeOptions; constructor(options: AuthorizationCodeOptions) { this.client = options.client; - this.ports = options.ports; this.context = options.context; - } - - private async openUrl(url: string) { - return open(url); + this.options = options; } public async fetch(scopes: OAuthScopes): Promise { @@ -84,7 +49,7 @@ export default class AuthorizationCode { let receivedParams: CallbackParamsType | undefined; - const server = await this.startServer((req, res) => { + const server = await this.createServer((req, res) => { const params = this.client.callbackParams(req); if (params.state === state) { receivedParams = params; @@ -108,7 +73,8 @@ export default class AuthorizationCode { redirect_uri: redirectUri, }); - await this.openUrl(authUrl); + const openAuthUrl = this.options.openAuthUrl ?? defaultOpenAuthUrl; + await openAuthUrl(authUrl, defaultOpenAuthUrl); await server.stopped(); if (!receivedParams || !receivedParams.code) { @@ -122,11 +88,11 @@ export default class AuthorizationCode { return { code: receivedParams.code, verifier: verifierString, redirectUri }; } - private async startServer(requestHandler: (req: IncomingMessage, res: ServerResponse) => void) { - for (const port of this.ports) { + private async createServer(requestHandler: (req: IncomingMessage, res: ServerResponse) => void) { + for (const port of this.options.ports) { const host = this.host; // eslint-disable-line prefer-destructuring try { - const server = await startServer(host, port, requestHandler); // eslint-disable-line no-await-in-loop + const server = await this.startServer(host, port, requestHandler); // eslint-disable-line no-await-in-loop this.context.getLogger().log(LogLevel.info, `Listening for OAuth authorization callback at ${host}:${port}`); let resolveStopped: () => void; @@ -140,7 +106,7 @@ export default class AuthorizationCode { host, port, server, - stop: () => stopServer(server).then(resolveStopped).catch(rejectStopped), + stop: () => this.stopServer(server).then(resolveStopped).catch(rejectStopped), stopped: () => stoppedPromise, }; } catch (error) { @@ -156,6 +122,50 @@ export default class AuthorizationCode { throw new AuthenticationError('Failed to start server: all ports are in use'); } + private createHttpServer(requestHandler: (req: IncomingMessage, res: ServerResponse) => void) { + return http.createServer(requestHandler); + } + + private async startServer( + host: string, + port: number, + requestHandler: (req: IncomingMessage, res: ServerResponse) => void, + ): Promise { + const server = this.createHttpServer(requestHandler); + + return new Promise((resolve, reject) => { + const errorListener = (error: Error) => { + server.off('error', errorListener); + reject(error); + }; + + server.on('error', errorListener); + server.listen(port, host, () => { + server.off('error', errorListener); + resolve(server); + }); + }); + } + + private async stopServer(server: Server): Promise { + if (!server.listening) { + return; + } + + return new Promise((resolve, reject) => { + const errorListener = (error: Error) => { + server.off('error', errorListener); + reject(error); + }; + + server.on('error', errorListener); + server.close(() => { + server.off('error', errorListener); + resolve(); + }); + }); + } + private renderCallbackResponse(): string { const applicationName = 'Databricks Sql Connector'; diff --git a/lib/connection/auth/DatabricksOAuth/index.ts b/lib/connection/auth/DatabricksOAuth/index.ts index faed4823..fa855af4 100644 --- a/lib/connection/auth/DatabricksOAuth/index.ts +++ b/lib/connection/auth/DatabricksOAuth/index.ts @@ -7,7 +7,7 @@ import IClientContext from '../../../contracts/IClientContext'; export { OAuthFlow }; -interface DatabricksOAuthOptions extends OAuthManagerOptions { +export interface DatabricksOAuthOptions extends OAuthManagerOptions { scopes?: OAuthScopes; persistence?: OAuthPersistence; headers?: HeadersInit; @@ -18,14 +18,13 @@ export default class DatabricksOAuth implements IAuthentication { private readonly options: DatabricksOAuthOptions; - private readonly manager: OAuthManager; + private manager?: OAuthManager; private readonly defaultPersistence = new OAuthPersistenceCache(); constructor(options: DatabricksOAuthOptions) { this.context = options.context; this.options = options; - this.manager = OAuthManager.getManager(this.options); } public async authenticate(): Promise { @@ -35,10 +34,10 @@ export default class DatabricksOAuth implements IAuthentication { let token = await persistence.read(host); if (!token) { - token = await this.manager.getToken(scopes ?? defaultOAuthScopes); + token = await this.getManager().getToken(scopes ?? defaultOAuthScopes); } - token = await this.manager.refreshAccessToken(token); + token = await this.getManager().refreshAccessToken(token); await persistence.persist(host, token); return { @@ -46,4 +45,11 @@ export default class DatabricksOAuth implements IAuthentication { Authorization: `Bearer ${token.accessToken}`, }; } + + private getManager(): OAuthManager { + if (!this.manager) { + this.manager = OAuthManager.getManager(this.options); + } + return this.manager; + } } diff --git a/lib/connection/connections/HttpConnection.ts b/lib/connection/connections/HttpConnection.ts index daa0661b..dc9c3902 100644 --- a/lib/connection/connections/HttpConnection.ts +++ b/lib/connection/connections/HttpConnection.ts @@ -36,7 +36,7 @@ export default class HttpConnection implements IConnectionProvider { }); } - public async getAgent(): Promise { + public async getAgent(): Promise { if (!this.agent) { if (this.options.proxy !== undefined) { this.agent = this.createProxyAgent(this.options.proxy); diff --git a/lib/connection/connections/HttpRetryPolicy.ts b/lib/connection/connections/HttpRetryPolicy.ts index 36506aee..c28d0efc 100644 --- a/lib/connection/connections/HttpRetryPolicy.ts +++ b/lib/connection/connections/HttpRetryPolicy.ts @@ -12,7 +12,7 @@ function delay(milliseconds: number): Promise { export default class HttpRetryPolicy implements IRetryPolicy { private context: IClientContext; - private readonly startTime: number; // in milliseconds + private startTime: number; // in milliseconds private attempt: number; diff --git a/lib/connection/contracts/IConnectionProvider.ts b/lib/connection/contracts/IConnectionProvider.ts index 08c21385..76406473 100644 --- a/lib/connection/contracts/IConnectionProvider.ts +++ b/lib/connection/contracts/IConnectionProvider.ts @@ -10,7 +10,7 @@ export interface HttpTransactionDetails { export default interface IConnectionProvider { getThriftConnection(): Promise; - getAgent(): Promise; + getAgent(): Promise; setHeaders(headers: HeadersInit): void; diff --git a/lib/contracts/IClientContext.ts b/lib/contracts/IClientContext.ts index 46c46c4b..6a70878d 100644 --- a/lib/contracts/IClientContext.ts +++ b/lib/contracts/IClientContext.ts @@ -1,7 +1,7 @@ import IDBSQLLogger from './IDBSQLLogger'; import IDriver from './IDriver'; import IConnectionProvider from '../connection/contracts/IConnectionProvider'; -import TCLIService from '../../thrift/TCLIService'; +import IThriftClient from './IThriftClient'; export interface ClientConfig { directResultsDefaultMaxRows: number; @@ -29,7 +29,7 @@ export default interface IClientContext { getConnectionProvider(): Promise; - getClient(): Promise; + getClient(): Promise; getDriver(): Promise; } diff --git a/lib/contracts/IThriftClient.ts b/lib/contracts/IThriftClient.ts new file mode 100644 index 00000000..9bfc3633 --- /dev/null +++ b/lib/contracts/IThriftClient.ts @@ -0,0 +1,9 @@ +import TCLIService from '../../thrift/TCLIService'; + +type ThriftClient = TCLIService.Client; + +type ThriftClientMethods = { + [K in keyof ThriftClient]: ThriftClient[K]; +}; + +export default interface IThriftClient extends ThriftClientMethods {} diff --git a/lib/hive/Commands/BaseCommand.ts b/lib/hive/Commands/BaseCommand.ts index 8a255a3a..22ac66fb 100644 --- a/lib/hive/Commands/BaseCommand.ts +++ b/lib/hive/Commands/BaseCommand.ts @@ -1,15 +1,14 @@ import { Response } from 'node-fetch'; -import TCLIService from '../../../thrift/TCLIService'; import HiveDriverError from '../../errors/HiveDriverError'; import RetryError, { RetryErrorCode } from '../../errors/RetryError'; import IClientContext from '../../contracts/IClientContext'; -export default abstract class BaseCommand { - protected client: TCLIService.Client; +export default abstract class BaseCommand { + protected client: ClientType; protected context: IClientContext; - constructor(client: TCLIService.Client, context: IClientContext) { + constructor(client: ClientType, context: IClientContext) { this.client = client; this.context = context; } diff --git a/lib/hive/Commands/CancelDelegationTokenCommand.ts b/lib/hive/Commands/CancelDelegationTokenCommand.ts index 376fb590..a9de93f1 100644 --- a/lib/hive/Commands/CancelDelegationTokenCommand.ts +++ b/lib/hive/Commands/CancelDelegationTokenCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TCancelDelegationTokenReq, TCancelDelegationTokenResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class CancelDelegationTokenCommand extends BaseCommand { +type Client = Pick; + +export default class CancelDelegationTokenCommand extends BaseCommand { execute(data: TCancelDelegationTokenReq): Promise { const request = new TCancelDelegationTokenReq(data); diff --git a/lib/hive/Commands/CancelOperationCommand.ts b/lib/hive/Commands/CancelOperationCommand.ts index dc8cd198..884f96a8 100644 --- a/lib/hive/Commands/CancelOperationCommand.ts +++ b/lib/hive/Commands/CancelOperationCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TCancelOperationReq, TCancelOperationResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class CancelOperationCommand extends BaseCommand { +type Client = Pick; + +export default class CancelOperationCommand extends BaseCommand { execute(data: TCancelOperationReq): Promise { const request = new TCancelOperationReq(data); diff --git a/lib/hive/Commands/CloseOperationCommand.ts b/lib/hive/Commands/CloseOperationCommand.ts index 340c541e..110eada1 100644 --- a/lib/hive/Commands/CloseOperationCommand.ts +++ b/lib/hive/Commands/CloseOperationCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TCloseOperationReq, TCloseOperationResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class CloseOperationCommand extends BaseCommand { +type Client = Pick; + +export default class CloseOperationCommand extends BaseCommand { execute(data: TCloseOperationReq): Promise { const request = new TCloseOperationReq(data); diff --git a/lib/hive/Commands/CloseSessionCommand.ts b/lib/hive/Commands/CloseSessionCommand.ts index 012179c7..2c2766b5 100644 --- a/lib/hive/Commands/CloseSessionCommand.ts +++ b/lib/hive/Commands/CloseSessionCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TCloseSessionReq, TCloseSessionResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class CloseSessionCommand extends BaseCommand { +type Client = Pick; + +export default class CloseSessionCommand extends BaseCommand { execute(openSessionRequest: TCloseSessionReq): Promise { const request = new TCloseSessionReq(openSessionRequest); diff --git a/lib/hive/Commands/ExecuteStatementCommand.ts b/lib/hive/Commands/ExecuteStatementCommand.ts index 98dd2aec..c44bdd40 100644 --- a/lib/hive/Commands/ExecuteStatementCommand.ts +++ b/lib/hive/Commands/ExecuteStatementCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TExecuteStatementReq, TExecuteStatementResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class ExecuteStatementCommand extends BaseCommand { +type Client = Pick; + +export default class ExecuteStatementCommand extends BaseCommand { execute(executeStatementRequest: TExecuteStatementReq): Promise { const request = new TExecuteStatementReq(executeStatementRequest); diff --git a/lib/hive/Commands/FetchResultsCommand.ts b/lib/hive/Commands/FetchResultsCommand.ts index 0637cac2..b216b037 100644 --- a/lib/hive/Commands/FetchResultsCommand.ts +++ b/lib/hive/Commands/FetchResultsCommand.ts @@ -1,10 +1,13 @@ import BaseCommand from './BaseCommand'; import { TFetchResultsReq, TFetchResultsResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; + +type Client = Pick; /** * TFetchResultsReq.fetchType - 0 represents Query output. 1 represents Log */ -export default class FetchResultsCommand extends BaseCommand { +export default class FetchResultsCommand extends BaseCommand { execute(data: TFetchResultsReq): Promise { const request = new TFetchResultsReq(data); diff --git a/lib/hive/Commands/GetCatalogsCommand.ts b/lib/hive/Commands/GetCatalogsCommand.ts index fbdccf93..aebd444f 100644 --- a/lib/hive/Commands/GetCatalogsCommand.ts +++ b/lib/hive/Commands/GetCatalogsCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetCatalogsReq, TGetCatalogsResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetCatalogsCommand extends BaseCommand { +type Client = Pick; + +export default class GetCatalogsCommand extends BaseCommand { execute(data: TGetCatalogsReq): Promise { const request = new TGetCatalogsReq(data); diff --git a/lib/hive/Commands/GetColumnsCommand.ts b/lib/hive/Commands/GetColumnsCommand.ts index 643ba2a7..f23034fa 100644 --- a/lib/hive/Commands/GetColumnsCommand.ts +++ b/lib/hive/Commands/GetColumnsCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetColumnsReq, TGetColumnsResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetColumnsCommand extends BaseCommand { +type Client = Pick; + +export default class GetColumnsCommand extends BaseCommand { execute(data: TGetColumnsReq): Promise { const request = new TGetColumnsReq(data); diff --git a/lib/hive/Commands/GetCrossReferenceCommand.ts b/lib/hive/Commands/GetCrossReferenceCommand.ts index 65d3de93..75c56b4b 100644 --- a/lib/hive/Commands/GetCrossReferenceCommand.ts +++ b/lib/hive/Commands/GetCrossReferenceCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetCrossReferenceReq, TGetCrossReferenceResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetCrossReferenceCommand extends BaseCommand { +type Client = Pick; + +export default class GetCrossReferenceCommand extends BaseCommand { execute(data: TGetCrossReferenceReq): Promise { const request = new TGetCrossReferenceReq(data); diff --git a/lib/hive/Commands/GetDelegationTokenCommand.ts b/lib/hive/Commands/GetDelegationTokenCommand.ts index bc21a78a..250a0c60 100644 --- a/lib/hive/Commands/GetDelegationTokenCommand.ts +++ b/lib/hive/Commands/GetDelegationTokenCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetDelegationTokenReq, TGetDelegationTokenResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetDelegationTokenCommand extends BaseCommand { +type Client = Pick; + +export default class GetDelegationTokenCommand extends BaseCommand { execute(data: TGetDelegationTokenReq): Promise { const request = new TGetDelegationTokenReq(data); diff --git a/lib/hive/Commands/GetFunctionsCommand.ts b/lib/hive/Commands/GetFunctionsCommand.ts index a21eb2cc..5880be82 100644 --- a/lib/hive/Commands/GetFunctionsCommand.ts +++ b/lib/hive/Commands/GetFunctionsCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetFunctionsReq, TGetFunctionsResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetFunctionsCommand extends BaseCommand { +type Client = Pick; + +export default class GetFunctionsCommand extends BaseCommand { execute(data: TGetFunctionsReq): Promise { const request = new TGetFunctionsReq(data); diff --git a/lib/hive/Commands/GetInfoCommand.ts b/lib/hive/Commands/GetInfoCommand.ts index bc5727d1..104bf793 100644 --- a/lib/hive/Commands/GetInfoCommand.ts +++ b/lib/hive/Commands/GetInfoCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetInfoReq, TGetInfoResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetInfoCommand extends BaseCommand { +type Client = Pick; + +export default class GetInfoCommand extends BaseCommand { execute(data: TGetInfoReq): Promise { const request = new TGetInfoReq(data); diff --git a/lib/hive/Commands/GetOperationStatusCommand.ts b/lib/hive/Commands/GetOperationStatusCommand.ts index ea6ce8af..d4eee154 100644 --- a/lib/hive/Commands/GetOperationStatusCommand.ts +++ b/lib/hive/Commands/GetOperationStatusCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetOperationStatusReq, TGetOperationStatusResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetOperationStatusCommand extends BaseCommand { +type Client = Pick; + +export default class GetOperationStatusCommand extends BaseCommand { execute(data: TGetOperationStatusReq): Promise { const request = new TGetOperationStatusReq(data); diff --git a/lib/hive/Commands/GetPrimaryKeysCommand.ts b/lib/hive/Commands/GetPrimaryKeysCommand.ts index aa2cf25a..34f2e3a1 100644 --- a/lib/hive/Commands/GetPrimaryKeysCommand.ts +++ b/lib/hive/Commands/GetPrimaryKeysCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetPrimaryKeysReq, TGetPrimaryKeysResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetPrimaryKeysCommand extends BaseCommand { +type Client = Pick; + +export default class GetPrimaryKeysCommand extends BaseCommand { execute(data: TGetPrimaryKeysReq): Promise { const request = new TGetPrimaryKeysReq(data); diff --git a/lib/hive/Commands/GetResultSetMetadataCommand.ts b/lib/hive/Commands/GetResultSetMetadataCommand.ts index cf62e09f..b1211836 100644 --- a/lib/hive/Commands/GetResultSetMetadataCommand.ts +++ b/lib/hive/Commands/GetResultSetMetadataCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetResultSetMetadataReq, TGetResultSetMetadataResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetResultSetMetadataCommand extends BaseCommand { +type Client = Pick; + +export default class GetResultSetMetadataCommand extends BaseCommand { execute(getResultSetMetadataRequest: TGetResultSetMetadataReq): Promise { const request = new TGetResultSetMetadataReq(getResultSetMetadataRequest); diff --git a/lib/hive/Commands/GetSchemasCommand.ts b/lib/hive/Commands/GetSchemasCommand.ts index d5488cfd..a0425db4 100644 --- a/lib/hive/Commands/GetSchemasCommand.ts +++ b/lib/hive/Commands/GetSchemasCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetSchemasReq, TGetSchemasResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetSchemasCommand extends BaseCommand { +type Client = Pick; + +export default class GetSchemasCommand extends BaseCommand { execute(data: TGetSchemasReq): Promise { const request = new TGetSchemasReq(data); diff --git a/lib/hive/Commands/GetTableTypesCommand.ts b/lib/hive/Commands/GetTableTypesCommand.ts index 1493beae..fee8261a 100644 --- a/lib/hive/Commands/GetTableTypesCommand.ts +++ b/lib/hive/Commands/GetTableTypesCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetTableTypesReq, TGetTableTypesResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetTableTypesCommand extends BaseCommand { +type Client = Pick; + +export default class GetTableTypesCommand extends BaseCommand { execute(data: TGetTableTypesReq): Promise { const request = new TGetTableTypesReq(data); diff --git a/lib/hive/Commands/GetTablesCommand.ts b/lib/hive/Commands/GetTablesCommand.ts index 5a3855c6..1d276c10 100644 --- a/lib/hive/Commands/GetTablesCommand.ts +++ b/lib/hive/Commands/GetTablesCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetTablesReq, TGetTablesResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetTablesCommand extends BaseCommand { +type Client = Pick; + +export default class GetTablesCommand extends BaseCommand { execute(data: TGetTablesReq): Promise { const request = new TGetTablesReq(data); diff --git a/lib/hive/Commands/GetTypeInfoCommand.ts b/lib/hive/Commands/GetTypeInfoCommand.ts index 33fc38c8..b0b80c8f 100644 --- a/lib/hive/Commands/GetTypeInfoCommand.ts +++ b/lib/hive/Commands/GetTypeInfoCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TGetTypeInfoReq, TGetTypeInfoResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class GetTypeInfoCommand extends BaseCommand { +type Client = Pick; + +export default class GetTypeInfoCommand extends BaseCommand { execute(data: TGetTypeInfoReq): Promise { const request = new TGetTypeInfoReq(data); diff --git a/lib/hive/Commands/OpenSessionCommand.ts b/lib/hive/Commands/OpenSessionCommand.ts index 203436ee..ef41259f 100644 --- a/lib/hive/Commands/OpenSessionCommand.ts +++ b/lib/hive/Commands/OpenSessionCommand.ts @@ -1,5 +1,8 @@ import BaseCommand from './BaseCommand'; import { TOpenSessionReq, TOpenSessionResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; + +type Client = Pick; /** * For auth mechanism GSSAPI the host and service should be provided when session is opened. @@ -10,7 +13,7 @@ import { TOpenSessionReq, TOpenSessionResp } from '../../../thrift/TCLIService_t * [key: string]: any; * } */ -export default class OpenSessionCommand extends BaseCommand { +export default class OpenSessionCommand extends BaseCommand { execute(openSessionRequest: TOpenSessionReq): Promise { const request = new TOpenSessionReq(openSessionRequest); diff --git a/lib/hive/Commands/RenewDelegationTokenCommand.ts b/lib/hive/Commands/RenewDelegationTokenCommand.ts index 902f3d5f..6f142cdb 100644 --- a/lib/hive/Commands/RenewDelegationTokenCommand.ts +++ b/lib/hive/Commands/RenewDelegationTokenCommand.ts @@ -1,7 +1,10 @@ import BaseCommand from './BaseCommand'; import { TRenewDelegationTokenReq, TRenewDelegationTokenResp } from '../../../thrift/TCLIService_types'; +import IThriftClient from '../../contracts/IThriftClient'; -export default class RenewDelegationTokenCommand extends BaseCommand { +type Client = Pick; + +export default class RenewDelegationTokenCommand extends BaseCommand { execute(data: TRenewDelegationTokenReq): Promise { const request = new TRenewDelegationTokenReq(data); diff --git a/lib/polyfills.ts b/lib/polyfills.ts index 006d38f1..113ccf3e 100644 --- a/lib/polyfills.ts +++ b/lib/polyfills.ts @@ -23,7 +23,7 @@ function toLength(value: unknown): number { } // https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.at -export function at(this: Array, index: number): T | undefined { +export function at(this: ArrayLike, index: unknown): T | undefined { const length = toLength(this.length); const relativeIndex = toIntegerOrInfinity(index); const absoluteIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex; diff --git a/lib/result/ArrowResultConverter.ts b/lib/result/ArrowResultConverter.ts index 5d8a5b1f..57fa02af 100644 --- a/lib/result/ArrowResultConverter.ts +++ b/lib/result/ArrowResultConverter.ts @@ -24,7 +24,7 @@ type ArrowSchema = Schema; type ArrowSchemaField = Field>; export default class ArrowResultConverter implements IResultsProvider> { - protected readonly context: IClientContext; + private readonly context: IClientContext; private readonly source: IResultsProvider; diff --git a/lib/result/ArrowResultHandler.ts b/lib/result/ArrowResultHandler.ts index 108f3365..a67cd617 100644 --- a/lib/result/ArrowResultHandler.ts +++ b/lib/result/ArrowResultHandler.ts @@ -6,7 +6,7 @@ import { ArrowBatch, hiveSchemaToArrowSchema } from './utils'; import { LZ4 } from '../utils'; export default class ArrowResultHandler implements IResultsProvider { - protected readonly context: IClientContext; + private readonly context: IClientContext; private readonly source: IResultsProvider; diff --git a/lib/result/CloudFetchResultHandler.ts b/lib/result/CloudFetchResultHandler.ts index c0450aef..081eb134 100644 --- a/lib/result/CloudFetchResultHandler.ts +++ b/lib/result/CloudFetchResultHandler.ts @@ -7,7 +7,7 @@ import { ArrowBatch } from './utils'; import { LZ4 } from '../utils'; export default class CloudFetchResultHandler implements IResultsProvider { - protected readonly context: IClientContext; + private readonly context: IClientContext; private readonly source: IResultsProvider; diff --git a/nyc.config.js b/nyc.config.js index 6a27b9fa..bf08a611 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -5,5 +5,5 @@ module.exports = { reporter: ['lcov'], all: true, include: ['lib/**'], - exclude: ['thrift/**', 'tests/**'], + exclude: ['lib/index.ts', 'thrift/**', 'tests/**'], }; diff --git a/package.json b/package.json index f379670a..162d6bcb 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ }, "scripts": { "prepare": "npm run build", - "e2e": "nyc --reporter=lcov --report-dir=${NYC_REPORT_DIR:-coverage_e2e} mocha --config tests/e2e/.mocharc.js", - "test": "nyc --reporter=lcov --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js", + "e2e": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_e2e} mocha --config tests/e2e/.mocharc.js", + "test": "nyc --report-dir=${NYC_REPORT_DIR:-coverage_unit} mocha --config tests/unit/.mocharc.js", "update-version": "node bin/update-version.js && prettier --write ./lib/version.ts", "build": "npm run update-version && tsc --project tsconfig.build.json", "watch": "tsc --project tsconfig.build.json --watch", diff --git a/tests/fixtures/compatibility/arrow/index.js b/tests/fixtures/compatibility/arrow/index.ts similarity index 56% rename from tests/fixtures/compatibility/arrow/index.js rename to tests/fixtures/compatibility/arrow/index.ts index a63b6e62..db7a0ef8 100644 --- a/tests/fixtures/compatibility/arrow/index.js +++ b/tests/fixtures/compatibility/arrow/index.ts @@ -1,18 +1,17 @@ -const Int64 = require('node-int64'); +import Int64 from 'node-int64'; +import fs from 'fs'; +import path from 'path'; -const fs = require('fs'); -const path = require('path'); +import schema from '../thrift_schema'; +import expectedData from '../expected'; +import { TRowSet } from '../../../../thrift/TCLIService_types'; -const thriftSchema = require('../thrift_schema'); const arrowSchema = fs.readFileSync(path.join(__dirname, 'schema.arrow')); const data = fs.readFileSync(path.join(__dirname, 'data.arrow')); -const expected = require('../expected'); -exports.schema = thriftSchema; +export { schema, arrowSchema }; -exports.arrowSchema = arrowSchema; - -exports.rowSets = [ +export const rowSets: Array = [ { startRowOffset: new Int64(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), 0), rows: [], @@ -25,7 +24,7 @@ exports.rowSets = [ }, ]; -exports.expected = expected.map((row) => ({ +export const expected = expectedData.map((row) => ({ ...row, dat: new Date(Date.parse(`${row.dat} UTC`)), })); diff --git a/tests/fixtures/compatibility/arrow_native_types/index.js b/tests/fixtures/compatibility/arrow_native_types/index.ts similarity index 58% rename from tests/fixtures/compatibility/arrow_native_types/index.js rename to tests/fixtures/compatibility/arrow_native_types/index.ts index ac01d1c2..13b3589e 100644 --- a/tests/fixtures/compatibility/arrow_native_types/index.js +++ b/tests/fixtures/compatibility/arrow_native_types/index.ts @@ -1,18 +1,17 @@ -const Int64 = require('node-int64'); +import Int64 from 'node-int64'; +import fs from 'fs'; +import path from 'path'; -const fs = require('fs'); -const path = require('path'); +import schema from '../thrift_schema'; +import expectedData from '../expected'; +import { TRowSet } from '../../../../thrift/TCLIService_types'; -const thriftSchema = require('../thrift_schema'); const arrowSchema = fs.readFileSync(path.join(__dirname, 'schema.arrow')); const data = fs.readFileSync(path.join(__dirname, 'data.arrow')); -const expected = require('../expected'); -exports.schema = thriftSchema; +export { schema, arrowSchema }; -exports.arrowSchema = arrowSchema; - -exports.rowSets = [ +export const rowSets: Array = [ { startRowOffset: new Int64(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), 0), rows: [], @@ -25,7 +24,7 @@ exports.rowSets = [ }, ]; -exports.expected = expected.map((row) => ({ +export const expected = expectedData.map((row) => ({ ...row, dat: new Date(Date.parse(`${row.dat} UTC`)), ts: new Date(Date.parse(`${row.ts} UTC`)), diff --git a/tests/fixtures/compatibility/column/data.js b/tests/fixtures/compatibility/column/data.js deleted file mode 100644 index 4eb54f4c..00000000 --- a/tests/fixtures/compatibility/column/data.js +++ /dev/null @@ -1,331 +0,0 @@ -const Int64 = require('node-int64'); - -module.exports = [ - { - boolVal: { - values: [true], - nulls: Buffer.from([0]), - }, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: { - values: [127], - nulls: Buffer.from([0]), - }, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: { - values: [32000], - nulls: Buffer.from([0]), - }, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: { - values: [4000000], - nulls: Buffer.from([0]), - }, - i64Val: null, - doubleVal: null, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: { - values: [new Int64(Buffer.from([0, 1, 82, 93, 148, 146, 127, 255]), 0)], - nulls: Buffer.from([0]), - }, - doubleVal: null, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: { - values: [1.4142], - nulls: Buffer.from([0]), - }, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: { - values: [2.71828182], - nulls: Buffer.from([0]), - }, - stringVal: null, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['3.14'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['string value'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['char value'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['varchar value'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['2014-01-17 00:17:13'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['2014-01-17'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['1 00:00:00.000000000'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['0-1'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: null, - binaryVal: { - values: [Buffer.from([98, 105, 110, 97, 114, 121, 32, 118, 97, 108, 117, 101])], - nulls: Buffer.from([0]), - }, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: [ - '{"bool":false,"int_type":4000000,"big_int":372036854775807,"dbl":2.71828182,"dec":1.4142,"str":"string value","arr1":[1.41,2.71,3.14],"arr2":[{"sqrt2":1.414},{"e":2.718},{"pi":3.142}],"arr3":[{"s":"e","d":2.71},{"s":"pi","d":3.14}],"map1":{"e":2.71,"pi":3.14,"sqrt2":1.41},"map2":{"arr1":[1.414],"arr2":[2.718,3.141]},"map3":{"struct1":{"d":3.14,"n":314159265359},"struct2":{"d":2.71,"n":271828182846}},"struct1":{"s":"string value","d":3.14,"n":314159265359,"a":[2.718,3.141]}}', - ], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['[1.41,2.71,3.14]'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['[{"sqrt2":1.4142},{"e":2.7182},{"pi":3.1415}]'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['[{"s":"sqrt2","d":1.41},{"s":"e","d":2.71},{"s":"pi","d":3.14}]'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['[[1.414],[2.718,3.141]]'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['{"e":2.71,"pi":3.14,"sqrt2":1.41}'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['{"arr1":[1.414],"arr2":[2.718,3.141]}'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['{"struct1":{"d":3.14,"n":314159265359},"struct2":{"d":2.71,"n":271828182846}}'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, - { - boolVal: null, - byteVal: null, - i16Val: null, - i32Val: null, - i64Val: null, - doubleVal: null, - stringVal: { - values: ['{"e":[271828182846],"pi":[314159265359]}'], - nulls: Buffer.from([0]), - }, - binaryVal: null, - }, -]; diff --git a/tests/fixtures/compatibility/column/data.ts b/tests/fixtures/compatibility/column/data.ts new file mode 100644 index 00000000..a2a9d2dc --- /dev/null +++ b/tests/fixtures/compatibility/column/data.ts @@ -0,0 +1,159 @@ +import Int64 from 'node-int64'; +import { TColumn } from '../../../../thrift/TCLIService_types'; + +const data: Array = [ + { + boolVal: { + values: [true], + nulls: Buffer.from([0]), + }, + }, + { + byteVal: { + values: [127], + nulls: Buffer.from([0]), + }, + }, + { + i16Val: { + values: [32000], + nulls: Buffer.from([0]), + }, + }, + { + i32Val: { + values: [4000000], + nulls: Buffer.from([0]), + }, + }, + { + i64Val: { + values: [new Int64(Buffer.from([0, 1, 82, 93, 148, 146, 127, 255]), 0)], + nulls: Buffer.from([0]), + }, + }, + { + doubleVal: { + values: [1.4142], + nulls: Buffer.from([0]), + }, + }, + { + doubleVal: { + values: [2.71828182], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['3.14'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['string value'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['char value'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['varchar value'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['2014-01-17 00:17:13'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['2014-01-17'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['1 00:00:00.000000000'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['0-1'], + nulls: Buffer.from([0]), + }, + }, + { + binaryVal: { + values: [Buffer.from([98, 105, 110, 97, 114, 121, 32, 118, 97, 108, 117, 101])], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: [ + '{"bool":false,"int_type":4000000,"big_int":372036854775807,"dbl":2.71828182,"dec":1.4142,"str":"string value","arr1":[1.41,2.71,3.14],"arr2":[{"sqrt2":1.414},{"e":2.718},{"pi":3.142}],"arr3":[{"s":"e","d":2.71},{"s":"pi","d":3.14}],"map1":{"e":2.71,"pi":3.14,"sqrt2":1.41},"map2":{"arr1":[1.414],"arr2":[2.718,3.141]},"map3":{"struct1":{"d":3.14,"n":314159265359},"struct2":{"d":2.71,"n":271828182846}},"struct1":{"s":"string value","d":3.14,"n":314159265359,"a":[2.718,3.141]}}', + ], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['[1.41,2.71,3.14]'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['[{"sqrt2":1.4142},{"e":2.7182},{"pi":3.1415}]'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['[{"s":"sqrt2","d":1.41},{"s":"e","d":2.71},{"s":"pi","d":3.14}]'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['[[1.414],[2.718,3.141]]'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['{"e":2.71,"pi":3.14,"sqrt2":1.41}'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['{"arr1":[1.414],"arr2":[2.718,3.141]}'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['{"struct1":{"d":3.14,"n":314159265359},"struct2":{"d":2.71,"n":271828182846}}'], + nulls: Buffer.from([0]), + }, + }, + { + stringVal: { + values: ['{"e":[271828182846],"pi":[314159265359]}'], + nulls: Buffer.from([0]), + }, + }, +]; + +export default data; diff --git a/tests/fixtures/compatibility/column/index.js b/tests/fixtures/compatibility/column/index.js deleted file mode 100644 index 22b4af1c..00000000 --- a/tests/fixtures/compatibility/column/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const Int64 = require('node-int64'); - -const thriftSchema = require('../thrift_schema'); -const data = require('./data'); -const expected = require('../expected'); - -exports.schema = thriftSchema; - -exports.rowSets = [ - { - startRowOffset: new Int64(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), 0), - rows: [], - columns: data, - }, -]; - -exports.expected = expected; diff --git a/tests/fixtures/compatibility/column/index.ts b/tests/fixtures/compatibility/column/index.ts new file mode 100644 index 00000000..9f42ad1b --- /dev/null +++ b/tests/fixtures/compatibility/column/index.ts @@ -0,0 +1,16 @@ +import Int64 from 'node-int64'; + +import schema from '../thrift_schema'; +import data from './data'; +import expected from '../expected'; +import { TRowSet } from '../../../../thrift/TCLIService_types'; + +export { schema, expected }; + +export const rowSets: Array = [ + { + startRowOffset: new Int64(Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), 0), + rows: [], + columns: data, + }, +]; diff --git a/tests/fixtures/compatibility/expected.js b/tests/fixtures/compatibility/expected.ts similarity index 98% rename from tests/fixtures/compatibility/expected.js rename to tests/fixtures/compatibility/expected.ts index ab88a645..a6f07c10 100644 --- a/tests/fixtures/compatibility/expected.js +++ b/tests/fixtures/compatibility/expected.ts @@ -1,4 +1,4 @@ -module.exports = [ +export default [ { bool: true, tiny_int: 127, diff --git a/tests/fixtures/compatibility/index.js b/tests/fixtures/compatibility/index.ts similarity index 75% rename from tests/fixtures/compatibility/index.js rename to tests/fixtures/compatibility/index.ts index 43247cdd..32f3b9ef 100644 --- a/tests/fixtures/compatibility/index.js +++ b/tests/fixtures/compatibility/index.ts @@ -1,10 +1,17 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; const createTableSql = fs.readFileSync(path.join(__dirname, 'create_table.sql')).toString(); const insertDataSql = fs.readFileSync(path.join(__dirname, 'insert_data.sql')).toString(); -function fixArrowResult(rows) { +export { createTableSql, insertDataSql }; + +type AllTypesRowShape = { + flt: number; + [key: string]: unknown; +}; + +export function fixArrowResult(rows: Array) { return rows.map((row) => ({ ...row, // This field is 32-bit floating point value, and since Arrow encodes it accurately, @@ -15,8 +22,3 @@ function fixArrowResult(rows) { flt: Number(row.flt.toFixed(4)), })); } - -exports.createTableSql = createTableSql; -exports.insertDataSql = insertDataSql; - -exports.fixArrowResult = fixArrowResult; diff --git a/tests/fixtures/compatibility/thrift_schema.js b/tests/fixtures/compatibility/thrift_schema.ts similarity index 85% rename from tests/fixtures/compatibility/thrift_schema.js rename to tests/fixtures/compatibility/thrift_schema.ts index fe45ea7f..5360cf07 100644 --- a/tests/fixtures/compatibility/thrift_schema.js +++ b/tests/fixtures/compatibility/thrift_schema.ts @@ -1,4 +1,6 @@ -module.exports = { +import { TTableSchema } from '../../../thrift/TCLIService_types'; + +const thriftSchema: TTableSchema = { columns: [ { columnName: 'bool', @@ -7,7 +9,6 @@ module.exports = { { primitiveEntry: { type: 0, - typeQualifiers: null, }, }, ], @@ -21,7 +22,6 @@ module.exports = { { primitiveEntry: { type: 1, - typeQualifiers: null, }, }, ], @@ -35,7 +35,6 @@ module.exports = { { primitiveEntry: { type: 2, - typeQualifiers: null, }, }, ], @@ -49,7 +48,6 @@ module.exports = { { primitiveEntry: { type: 3, - typeQualifiers: null, }, }, ], @@ -63,7 +61,6 @@ module.exports = { { primitiveEntry: { type: 4, - typeQualifiers: null, }, }, ], @@ -77,7 +74,6 @@ module.exports = { { primitiveEntry: { type: 5, - typeQualifiers: null, }, }, ], @@ -91,7 +87,6 @@ module.exports = { { primitiveEntry: { type: 6, - typeQualifiers: null, }, }, ], @@ -124,7 +119,6 @@ module.exports = { { primitiveEntry: { type: 7, - typeQualifiers: null, }, }, ], @@ -138,7 +132,6 @@ module.exports = { { primitiveEntry: { type: 7, - typeQualifiers: null, }, }, ], @@ -152,7 +145,6 @@ module.exports = { { primitiveEntry: { type: 7, - typeQualifiers: null, }, }, ], @@ -166,7 +158,6 @@ module.exports = { { primitiveEntry: { type: 8, - typeQualifiers: null, }, }, ], @@ -180,7 +171,6 @@ module.exports = { { primitiveEntry: { type: 17, - typeQualifiers: null, }, }, ], @@ -194,7 +184,6 @@ module.exports = { { primitiveEntry: { type: 7, - typeQualifiers: null, }, }, ], @@ -208,7 +197,6 @@ module.exports = { { primitiveEntry: { type: 7, - typeQualifiers: null, }, }, ], @@ -222,7 +210,6 @@ module.exports = { { primitiveEntry: { type: 9, - typeQualifiers: null, }, }, ], @@ -236,7 +223,6 @@ module.exports = { { primitiveEntry: { type: 12, - typeQualifiers: null, }, }, ], @@ -250,7 +236,6 @@ module.exports = { { primitiveEntry: { type: 10, - typeQualifiers: null, }, }, ], @@ -264,7 +249,6 @@ module.exports = { { primitiveEntry: { type: 10, - typeQualifiers: null, }, }, ], @@ -278,7 +262,6 @@ module.exports = { { primitiveEntry: { type: 10, - typeQualifiers: null, }, }, ], @@ -292,7 +275,6 @@ module.exports = { { primitiveEntry: { type: 10, - typeQualifiers: null, }, }, ], @@ -306,7 +288,6 @@ module.exports = { { primitiveEntry: { type: 11, - typeQualifiers: null, }, }, ], @@ -320,7 +301,6 @@ module.exports = { { primitiveEntry: { type: 11, - typeQualifiers: null, }, }, ], @@ -334,7 +314,6 @@ module.exports = { { primitiveEntry: { type: 11, - typeQualifiers: null, }, }, ], @@ -348,7 +327,6 @@ module.exports = { { primitiveEntry: { type: 11, - typeQualifiers: null, }, }, ], @@ -357,3 +335,5 @@ module.exports = { }, ], }; + +export default thriftSchema; diff --git a/tests/unit/.mocharc.js b/tests/unit/.mocharc.js index 4f62b161..8dcbd98e 100644 --- a/tests/unit/.mocharc.js +++ b/tests/unit/.mocharc.js @@ -1,6 +1,6 @@ 'use strict'; -const allSpecs = 'tests/unit/**/*.test.js'; +const allSpecs = 'tests/unit/**/*.test.ts'; const argvSpecs = process.argv.slice(4); diff --git a/tests/unit/.stubs/AuthProviderStub.ts b/tests/unit/.stubs/AuthProviderStub.ts new file mode 100644 index 00000000..ed117d2f --- /dev/null +++ b/tests/unit/.stubs/AuthProviderStub.ts @@ -0,0 +1,14 @@ +import { HeadersInit } from 'node-fetch'; +import IAuthentication from '../../../lib/connection/contracts/IAuthentication'; + +export default class AuthProviderStub implements IAuthentication { + public headers: HeadersInit; + + constructor(headers: HeadersInit = {}) { + this.headers = headers; + } + + public async authenticate() { + return this.headers; + } +} diff --git a/tests/unit/.stubs/ClientContextStub.ts b/tests/unit/.stubs/ClientContextStub.ts new file mode 100644 index 00000000..519316ff --- /dev/null +++ b/tests/unit/.stubs/ClientContextStub.ts @@ -0,0 +1,51 @@ +import IClientContext, { ClientConfig } from '../../../lib/contracts/IClientContext'; +import IConnectionProvider from '../../../lib/connection/contracts/IConnectionProvider'; +import IDriver from '../../../lib/contracts/IDriver'; +import IThriftClient from '../../../lib/contracts/IThriftClient'; +import IDBSQLLogger from '../../../lib/contracts/IDBSQLLogger'; +import DBSQLClient from '../../../lib/DBSQLClient'; + +import LoggerStub from './LoggerStub'; +import ThriftClientStub from './ThriftClientStub'; +import DriverStub from './DriverStub'; +import ConnectionProviderStub from './ConnectionProviderStub'; + +export default class ClientContextStub implements IClientContext { + public configOverrides: Partial; + + public logger = new LoggerStub(); + + public thriftClient = new ThriftClientStub(); + + public driver = new DriverStub(); + + public connectionProvider = new ConnectionProviderStub(); + + constructor(configOverrides: Partial = {}) { + this.configOverrides = configOverrides; + } + + public getConfig(): ClientConfig { + const defaultConfig = DBSQLClient['getDefaultConfig'](); + return { + ...defaultConfig, + ...this.configOverrides, + }; + } + + public getLogger(): IDBSQLLogger { + return this.logger; + } + + public async getConnectionProvider(): Promise { + return this.connectionProvider; + } + + public async getClient(): Promise { + return this.thriftClient; + } + + public async getDriver(): Promise { + return this.driver; + } +} diff --git a/tests/unit/.stubs/ConnectionProviderStub.ts b/tests/unit/.stubs/ConnectionProviderStub.ts new file mode 100644 index 00000000..20b54dfd --- /dev/null +++ b/tests/unit/.stubs/ConnectionProviderStub.ts @@ -0,0 +1,25 @@ +import http from 'http'; +import { HeadersInit } from 'node-fetch'; +import IConnectionProvider, { HttpTransactionDetails } from '../../../lib/connection/contracts/IConnectionProvider'; +import IRetryPolicy from '../../../lib/connection/contracts/IRetryPolicy'; +import NullRetryPolicy from '../../../lib/connection/connections/NullRetryPolicy'; + +export default class ConnectionProviderStub implements IConnectionProvider { + public headers: HeadersInit = {}; + + public async getThriftConnection(): Promise { + return {}; + } + + public async getAgent(): Promise { + return undefined; + } + + public setHeaders(headers: HeadersInit) { + this.headers = headers; + } + + public async getRetryPolicy(): Promise> { + return new NullRetryPolicy(); + } +} diff --git a/tests/unit/.stubs/DriverStub.ts b/tests/unit/.stubs/DriverStub.ts new file mode 100644 index 00000000..339941ed --- /dev/null +++ b/tests/unit/.stubs/DriverStub.ts @@ -0,0 +1,370 @@ +import Int64 from 'node-int64'; +import IDriver from '../../../lib/contracts/IDriver'; +import { + TCancelDelegationTokenReq, + TCancelDelegationTokenResp, + TCancelOperationReq, + TCancelOperationResp, + TCloseOperationReq, + TCloseOperationResp, + TCloseSessionReq, + TCloseSessionResp, + TExecuteStatementReq, + TExecuteStatementResp, + TFetchResultsReq, + TFetchResultsResp, + TGetCatalogsReq, + TGetCatalogsResp, + TGetColumnsReq, + TGetColumnsResp, + TGetCrossReferenceReq, + TGetCrossReferenceResp, + TGetDelegationTokenReq, + TGetDelegationTokenResp, + TGetFunctionsReq, + TGetFunctionsResp, + TGetInfoReq, + TGetInfoResp, + TGetOperationStatusReq, + TGetOperationStatusResp, + TGetPrimaryKeysReq, + TGetPrimaryKeysResp, + TGetResultSetMetadataReq, + TGetResultSetMetadataResp, + TGetSchemasReq, + TGetSchemasResp, + TGetTablesReq, + TGetTablesResp, + TGetTableTypesReq, + TGetTableTypesResp, + TGetTypeInfoReq, + TGetTypeInfoResp, + TOpenSessionReq, + TOpenSessionResp, + TOperationState, + TOperationType, + TProtocolVersion, + TRenewDelegationTokenReq, + TRenewDelegationTokenResp, + TSparkRowSetType, + TStatusCode, + TTypeId, +} from '../../../thrift/TCLIService_types'; + +export default class DriverStub implements IDriver { + public openSessionReq?: TOpenSessionReq; + + public openSessionResp: TOpenSessionResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + serverProtocolVersion: TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V8, + }; + + public async openSession(req: TOpenSessionReq) { + this.openSessionReq = req; + return this.openSessionResp; + } + + public closeSessionReq?: TCloseSessionReq; + + public closeSessionResp: TCloseSessionResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public async closeSession(req: TCloseSessionReq) { + this.closeSessionReq = req; + return this.closeSessionResp; + } + + public getInfoReq?: TGetInfoReq; + + public getInfoResp: TGetInfoResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + infoValue: { stringValue: 'test' }, + }; + + public async getInfo(req: TGetInfoReq) { + this.getInfoReq = req; + return this.getInfoResp; + } + + public executeStatementReq?: TExecuteStatementReq; + + public executeStatementResp: TExecuteStatementResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async executeStatement(req: TExecuteStatementReq) { + this.executeStatementReq = req; + return this.executeStatementResp; + } + + public getTypeInfoReq?: TGetTypeInfoReq; + + public getTypeInfoResp: TGetTypeInfoResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getTypeInfo(req: TGetTypeInfoReq) { + this.getTypeInfoReq = req; + return this.getTypeInfoResp; + } + + public getCatalogsReq?: TGetCatalogsReq; + + public getCatalogsResp: TGetCatalogsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getCatalogs(req: TGetCatalogsReq) { + this.getCatalogsReq = req; + return this.getCatalogsResp; + } + + public getSchemasReq?: TGetSchemasReq; + + public getSchemasResp: TGetSchemasResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getSchemas(req: TGetSchemasReq) { + this.getSchemasReq = req; + return this.getSchemasResp; + } + + public getTablesReq?: TGetTablesReq; + + public getTablesResp: TGetTablesResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getTables(req: TGetTablesReq) { + this.getTablesReq = req; + return this.getTablesResp; + } + + public getTableTypesReq?: TGetTableTypesReq; + + public getTableTypesResp: TGetTableTypesResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getTableTypes(req: TGetTableTypesReq) { + this.getTableTypesReq = req; + return this.getTableTypesResp; + } + + public getColumnsReq?: TGetColumnsReq; + + public getColumnsResp: TGetColumnsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getColumns(req: TGetColumnsReq) { + this.getColumnsReq = req; + return this.getColumnsResp; + } + + public getFunctionsReq?: TGetFunctionsReq; + + public getFunctionsResp: TGetFunctionsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getFunctions(req: TGetFunctionsReq) { + this.getFunctionsReq = req; + return this.getFunctionsResp; + } + + public getPrimaryKeysReq?: TGetPrimaryKeysReq; + + public getPrimaryKeysResp: TGetPrimaryKeysResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getPrimaryKeys(req: TGetPrimaryKeysReq) { + this.getPrimaryKeysReq = req; + return this.getPrimaryKeysResp; + } + + public getCrossReferenceReq?: TGetCrossReferenceReq; + + public getCrossReferenceResp: TGetCrossReferenceResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public async getCrossReference(req: TGetCrossReferenceReq) { + this.getCrossReferenceReq = req; + return this.getCrossReferenceResp; + } + + public getOperationStatusReq?: TGetOperationStatusReq; + + public getOperationStatusResp: TGetOperationStatusResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + }; + + public async getOperationStatus(req: TGetOperationStatusReq) { + this.getOperationStatusReq = req; + return this.getOperationStatusResp; + } + + public cancelOperationReq?: TCancelOperationReq; + + public cancelOperationResp: TCancelOperationResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public async cancelOperation(req: TCancelOperationReq) { + this.cancelOperationReq = req; + return this.cancelOperationResp; + } + + public closeOperationReq?: TCloseOperationReq; + + public closeOperationResp: TCloseOperationResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public async closeOperation(req: TCloseOperationReq) { + this.closeOperationReq = req; + return this.closeOperationResp; + } + + public getResultSetMetadataReq?: TGetResultSetMetadataReq; + + public getResultSetMetadataResp: TGetResultSetMetadataResp = { + status: { statusCode: 0 }, + resultFormat: TSparkRowSetType.COLUMN_BASED_SET, + schema: { + columns: [ + { + columnName: 'test', + typeDesc: { + types: [ + { + primitiveEntry: { + type: TTypeId.STRING_TYPE, + }, + }, + ], + }, + position: 1, + comment: '', + }, + ], + }, + }; + + public async getResultSetMetadata(req: TGetResultSetMetadataReq) { + this.getResultSetMetadataReq = req; + return this.getResultSetMetadataResp; + } + + public fetchResultsReq?: TFetchResultsReq; + + public fetchResultsResp: TFetchResultsResp = { + status: { statusCode: 0 }, + hasMoreRows: false, + results: { + startRowOffset: new Int64(0), + rows: [], + columns: [ + { + stringVal: { values: ['a', 'b', 'c'], nulls: Buffer.from([]) }, + }, + ], + binaryColumns: Buffer.from([]), + columnCount: 2, + }, + }; + + public async fetchResults(req: TFetchResultsReq) { + this.fetchResultsReq = req; + return this.fetchResultsResp; + } + + public getDelegationTokenReq?: TGetDelegationTokenReq; + + public getDelegationTokenResp: TGetDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + delegationToken: 'token', + }; + + public async getDelegationToken(req: TGetDelegationTokenReq) { + this.getDelegationTokenReq = req; + return this.getDelegationTokenResp; + } + + public cancelDelegationTokenReq?: TCancelDelegationTokenReq; + + public cancelDelegationTokenResp: TCancelDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public async cancelDelegationToken(req: TCancelDelegationTokenReq) { + this.cancelDelegationTokenReq = req; + return this.cancelDelegationTokenResp; + } + + public renewDelegationTokenReq?: TRenewDelegationTokenReq; + + public renewDelegationTokenResp: TRenewDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public async renewDelegationToken(req: TRenewDelegationTokenReq) { + this.renewDelegationTokenReq = req; + return this.renewDelegationTokenResp; + } +} diff --git a/tests/unit/.stubs/LoggerStub.ts b/tests/unit/.stubs/LoggerStub.ts new file mode 100644 index 00000000..0b10f6c0 --- /dev/null +++ b/tests/unit/.stubs/LoggerStub.ts @@ -0,0 +1,7 @@ +import IDBSQLLogger, { LogLevel } from '../../../lib/contracts/IDBSQLLogger'; + +export default class LoggerStub implements IDBSQLLogger { + public log(level: LogLevel, message: string) { + // do nothing + } +} diff --git a/tests/unit/.stubs/OAuth.ts b/tests/unit/.stubs/OAuth.ts new file mode 100644 index 00000000..bd7026f2 --- /dev/null +++ b/tests/unit/.stubs/OAuth.ts @@ -0,0 +1,195 @@ +import { expect } from 'chai'; +import OAuthManager from '../../../lib/connection/auth/DatabricksOAuth/OAuthManager'; +import OAuthToken from '../../../lib/connection/auth/DatabricksOAuth/OAuthToken'; +import { OAuthScopes, scopeDelimiter } from '../../../lib/connection/auth/DatabricksOAuth/OAuthScope'; +import OAuthPersistence from '../../../lib/connection/auth/DatabricksOAuth/OAuthPersistence'; +import { EventEmitter } from 'events'; +import { IncomingMessage, ServerResponse, RequestListener } from 'http'; +import { ListenOptions } from 'net'; + +export function createAccessToken(expirationTime: number) { + const payload = Buffer.from(JSON.stringify({ exp: expirationTime }), 'utf8').toString('base64'); + return `access.${payload}`; +} + +export function createValidAccessToken() { + const expirationTime = Math.trunc(Date.now() / 1000) + 20000; + return createAccessToken(expirationTime); +} + +export function createExpiredAccessToken() { + const expirationTime = Math.trunc(Date.now() / 1000) - 1000; + return createAccessToken(expirationTime); +} + +export class OAuthPersistenceStub implements OAuthPersistence { + public token?: OAuthToken; + + async persist(host: string, token: OAuthToken) { + this.token = token; + } + + async read() { + return this.token; + } +} + +export class OAuthCallbackServerStub< + Request extends typeof IncomingMessage = typeof IncomingMessage, + Response extends typeof ServerResponse = typeof ServerResponse, +> extends EventEmitter { + public requestHandler: RequestListener; + + public listening = false; + + public listenError?: Error; // error to emit on listen + + public closeError?: Error; // error to emit on close + + constructor(requestHandler?: RequestListener) { + super(); + this.requestHandler = + requestHandler ?? + (() => { + throw new Error('OAuthCallbackServerStub: no request handler provided'); + }); + } + + // We support only one of these signatures, but have to declare all for compatibility with `http.Server` + listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, hostname?: string, listeningListener?: () => void): this; + listen(port?: number, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, listeningListener?: () => void): this; + listen(path: string, backlog?: number, listeningListener?: () => void): this; + listen(path: string, listeningListener?: () => void): this; + listen(options: ListenOptions, listeningListener?: () => void): this; + listen(handle: any, backlog?: number, listeningListener?: () => void): this; + listen(handle: any, listeningListener?: () => void): this; + listen(...args: unknown[]) { + const [port, host, callback] = args; + + if (typeof port !== 'number' || typeof host !== 'string' || typeof callback !== 'function') { + throw new TypeError('Only this signature supported: `listen(port: number, host: string, callback: () => void)`'); + } + + if (this.listenError) { + this.emit('error', this.listenError); + this.listenError = undefined; + } else if (port < 1000) { + const error = new Error(`Address ${host}:${port} is already in use`); + (error as any).code = 'EADDRINUSE'; + this.emit('error', error); + } else { + this.listening = true; + callback(); + } + + return this; + } + + close(callback: () => void) { + this.requestHandler = () => {}; + this.listening = false; + if (this.closeError) { + this.emit('error', this.closeError); + this.closeError = undefined; + } else { + callback(); + } + + return this; + } + + // Dummy methods and properties for compatibility with `http.Server` + + public maxHeadersCount: number | null = null; + + public maxRequestsPerSocket: number | null = null; + + public timeout: number = -1; + + public headersTimeout: number = -1; + + public keepAliveTimeout: number = -1; + + public requestTimeout: number = -1; + + public maxConnections: number = -1; + + public connections: number = 0; + + public setTimeout() { + return this; + } + + public closeAllConnections() {} + + public closeIdleConnections() {} + + public address() { + return null; + } + + public getConnections() {} + + public ref() { + return this; + } + + public unref() { + return this; + } +} + +export class AuthorizationCodeStub { + public fetchResult: unknown = undefined; + + public expectedScope?: string = undefined; + + static validCode = { + code: 'auth_code', + verifier: 'verifier_string', + redirectUri: 'http://localhost:8000', + }; + + async fetch(scopes: Array) { + if (this.expectedScope) { + expect(scopes.join(scopeDelimiter)).to.be.equal(this.expectedScope); + } + return this.fetchResult; + } +} + +export class OAuthManagerStub extends OAuthManager { + public getTokenResult = new OAuthToken(createValidAccessToken()); + + public refreshTokenResult = new OAuthToken(createValidAccessToken()); + + protected getOIDCConfigUrl(): string { + throw new Error('Not implemented'); + } + + protected getAuthorizationUrl(): string { + throw new Error('Not implemented'); + } + + protected getClientId(): string { + throw new Error('Not implemented'); + } + + protected getCallbackPorts(): Array { + throw new Error('Not implemented'); + } + + protected getScopes(requestedScopes: OAuthScopes) { + return requestedScopes; + } + + public async refreshAccessToken(token: OAuthToken) { + return token.hasExpired ? this.refreshTokenResult : token; + } + + public async getToken() { + return this.getTokenResult; + } +} diff --git a/tests/unit/.stubs/OperationStub.ts b/tests/unit/.stubs/OperationStub.ts new file mode 100644 index 00000000..19a9087b --- /dev/null +++ b/tests/unit/.stubs/OperationStub.ts @@ -0,0 +1,62 @@ +import IOperation, { + IOperationChunksIterator, + IOperationRowsIterator, + IteratorOptions, +} from '../../../lib/contracts/IOperation'; +import Status from '../../../lib/dto/Status'; +import { OperationChunksIterator, OperationRowsIterator } from '../../../lib/utils/OperationIterator'; + +export default class OperationStub implements IOperation { + public readonly id: string = ''; + + private chunks: Array>; + public closed: boolean; + + constructor(chunks: Array>) { + this.chunks = Array.isArray(chunks) ? [...chunks] : []; + this.closed = false; + } + + public async fetchChunk() { + return this.chunks.shift() ?? []; + } + + public async fetchAll() { + const result = this.chunks.flat(); + this.chunks = []; + return result; + } + + public async status() { + return Promise.reject(new Error('Not implemented')); + } + + public async cancel() { + return Promise.reject(new Error('Not implemented')); + } + + public async close() { + this.closed = true; + return Status.success(); + } + + public async finished() { + return Promise.resolve(); + } + + public async hasMoreRows() { + return !this.closed && this.chunks.length > 0; + } + + public async getSchema() { + return Promise.reject(new Error('Not implemented')); + } + + public iterateChunks(options?: IteratorOptions): IOperationChunksIterator { + return new OperationChunksIterator(this, options); + } + + public iterateRows(options?: IteratorOptions): IOperationRowsIterator { + return new OperationRowsIterator(this, options); + } +} diff --git a/tests/unit/.stubs/ResultsProviderStub.ts b/tests/unit/.stubs/ResultsProviderStub.ts new file mode 100644 index 00000000..90bc7227 --- /dev/null +++ b/tests/unit/.stubs/ResultsProviderStub.ts @@ -0,0 +1,20 @@ +import IResultsProvider from '../../../lib/result/IResultsProvider'; + +export default class ResultsProviderStub implements IResultsProvider { + private readonly items: Array; + + private readonly emptyItem: T; + + constructor(items: Array, emptyItem: T) { + this.items = [...items]; + this.emptyItem = emptyItem; + } + + async hasMore() { + return this.items.length > 0; + } + + async fetchNext() { + return this.items.shift() ?? this.emptyItem; + } +} diff --git a/tests/unit/.stubs/ThriftClientStub.ts b/tests/unit/.stubs/ThriftClientStub.ts new file mode 100644 index 00000000..9cc81059 --- /dev/null +++ b/tests/unit/.stubs/ThriftClientStub.ts @@ -0,0 +1,393 @@ +import Int64 from 'node-int64'; +import IThriftClient from '../../../lib/contracts/IThriftClient'; +import { + TCancelDelegationTokenReq, + TCancelDelegationTokenResp, + TCancelOperationReq, + TCancelOperationResp, + TCloseOperationReq, + TCloseOperationResp, + TCloseSessionReq, + TCloseSessionResp, + TExecuteStatementReq, + TExecuteStatementResp, + TFetchResultsReq, + TFetchResultsResp, + TGetCatalogsReq, + TGetCatalogsResp, + TGetColumnsReq, + TGetColumnsResp, + TGetCrossReferenceReq, + TGetCrossReferenceResp, + TGetDelegationTokenReq, + TGetDelegationTokenResp, + TGetFunctionsReq, + TGetFunctionsResp, + TGetInfoReq, + TGetInfoResp, + TGetOperationStatusReq, + TGetOperationStatusResp, + TGetPrimaryKeysReq, + TGetPrimaryKeysResp, + TGetResultSetMetadataReq, + TGetResultSetMetadataResp, + TGetSchemasReq, + TGetSchemasResp, + TGetTablesReq, + TGetTablesResp, + TGetTableTypesReq, + TGetTableTypesResp, + TGetTypeInfoReq, + TGetTypeInfoResp, + TOpenSessionReq, + TOpenSessionResp, + TOperationState, + TOperationType, + TProtocolVersion, + TRenewDelegationTokenReq, + TRenewDelegationTokenResp, + TSparkRowSetType, + TStatusCode, + TTypeId, +} from '../../../thrift/TCLIService_types'; + +export type ThriftClientCommandCallback = (error: void, resp: R) => void; + +export default class ThriftClientStub implements IThriftClient { + public openSessionReq?: TOpenSessionReq; + + public openSessionResp: TOpenSessionResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + serverProtocolVersion: TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V8, + }; + + public OpenSession(req: TOpenSessionReq, callback?: ThriftClientCommandCallback) { + this.openSessionReq = req; + callback?.(undefined, this.openSessionResp); + } + + public closeSessionReq?: TCloseSessionReq; + + public closeSessionResp: TCloseSessionResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public CloseSession(req: TCloseSessionReq, callback?: ThriftClientCommandCallback) { + this.closeSessionReq = req; + callback?.(undefined, this.closeSessionResp); + } + + public getInfoReq?: TGetInfoReq; + + public getInfoResp: TGetInfoResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + infoValue: { stringValue: 'test' }, + }; + + public GetInfo(req: TGetInfoReq, callback?: ThriftClientCommandCallback) { + this.getInfoReq = req; + callback?.(undefined, this.getInfoResp); + } + + public executeStatementReq?: TExecuteStatementReq; + + public executeStatementResp: TExecuteStatementResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public ExecuteStatement(req: TExecuteStatementReq, callback?: ThriftClientCommandCallback) { + this.executeStatementReq = req; + callback?.(undefined, this.executeStatementResp); + } + + public getTypeInfoReq?: TGetTypeInfoReq; + + public getTypeInfoResp: TGetTypeInfoResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetTypeInfo(req: TGetTypeInfoReq, callback?: ThriftClientCommandCallback) { + this.getTypeInfoReq = req; + callback?.(undefined, this.getTypeInfoResp); + } + + public getCatalogsReq?: TGetCatalogsReq; + + public getCatalogsResp: TGetCatalogsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetCatalogs(req: TGetCatalogsReq, callback?: ThriftClientCommandCallback) { + this.getCatalogsReq = req; + callback?.(undefined, this.getCatalogsResp); + } + + public getSchemasReq?: TGetSchemasReq; + + public getSchemasResp: TGetSchemasResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetSchemas(req: TGetSchemasReq, callback?: ThriftClientCommandCallback) { + this.getSchemasReq = req; + callback?.(undefined, this.getSchemasResp); + } + + public getTablesReq?: TGetTablesReq; + + public getTablesResp: TGetTablesResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetTables(req: TGetTablesReq, callback?: ThriftClientCommandCallback) { + this.getTablesReq = req; + callback?.(undefined, this.getTablesResp); + } + + public getTableTypesReq?: TGetTableTypesReq; + + public getTableTypesResp: TGetTableTypesResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetTableTypes(req: TGetTableTypesReq, callback?: ThriftClientCommandCallback) { + this.getTableTypesReq = req; + callback?.(undefined, this.getTableTypesResp); + } + + public getColumnsReq?: TGetColumnsReq; + + public getColumnsResp: TGetColumnsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetColumns(req: TGetColumnsReq, callback?: ThriftClientCommandCallback) { + this.getColumnsReq = req; + callback?.(undefined, this.getColumnsResp); + } + + public getFunctionsReq?: TGetFunctionsReq; + + public getFunctionsResp: TGetFunctionsResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetFunctions(req: TGetFunctionsReq, callback?: ThriftClientCommandCallback) { + this.getFunctionsReq = req; + callback?.(undefined, this.getFunctionsResp); + } + + public getPrimaryKeysReq?: TGetPrimaryKeysReq; + + public getPrimaryKeysResp: TGetPrimaryKeysResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetPrimaryKeys(req: TGetPrimaryKeysReq, callback?: ThriftClientCommandCallback) { + this.getPrimaryKeysReq = req; + callback?.(undefined, this.getPrimaryKeysResp); + } + + public getCrossReferenceReq?: TGetCrossReferenceReq; + + public getCrossReferenceResp: TGetCrossReferenceResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: false, + }, + }; + + public GetCrossReference(req: TGetCrossReferenceReq, callback?: ThriftClientCommandCallback) { + this.getCrossReferenceReq = req; + callback?.(undefined, this.getCrossReferenceResp); + } + + public getOperationStatusReq?: TGetOperationStatusReq; + + public getOperationStatusResp: TGetOperationStatusResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + }; + + public GetOperationStatus( + req: TGetOperationStatusReq, + callback?: ThriftClientCommandCallback, + ) { + this.getOperationStatusReq = req; + callback?.(undefined, this.getOperationStatusResp); + } + + public cancelOperationReq?: TCancelOperationReq; + + public cancelOperationResp: TCancelOperationResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public CancelOperation(req: TCancelOperationReq, callback?: ThriftClientCommandCallback) { + this.cancelOperationReq = req; + callback?.(undefined, this.cancelOperationResp); + } + + public closeOperationReq?: TCloseOperationReq; + + public closeOperationResp: TCloseOperationResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public CloseOperation(req: TCloseOperationReq, callback?: ThriftClientCommandCallback) { + this.closeOperationReq = req; + callback?.(undefined, this.closeOperationResp); + } + + public getResultSetMetadataReq?: TGetResultSetMetadataReq; + + public getResultSetMetadataResp: TGetResultSetMetadataResp = { + status: { statusCode: 0 }, + resultFormat: TSparkRowSetType.COLUMN_BASED_SET, + schema: { + columns: [ + { + columnName: 'column1', + typeDesc: { + types: [ + { + primitiveEntry: { + type: TTypeId.STRING_TYPE, + }, + }, + ], + }, + position: 0, + comment: '', + }, + ], + }, + }; + + public GetResultSetMetadata( + req: TGetResultSetMetadataReq, + callback?: ThriftClientCommandCallback, + ) { + this.getResultSetMetadataReq = req; + callback?.(undefined, this.getResultSetMetadataResp); + } + + public fetchResultsReq?: TFetchResultsReq; + + public fetchResultsResp: TFetchResultsResp = { + status: { statusCode: 0 }, + hasMoreRows: false, + results: { + startRowOffset: new Int64(0), + rows: [ + { + colVals: [{ boolVal: { value: true } }, { stringVal: { value: 'value' } }], + }, + ], + columns: [ + { boolVal: { values: [true], nulls: Buffer.from([]) } }, + { stringVal: { values: ['value'], nulls: Buffer.from([]) } }, + ], + binaryColumns: Buffer.from([]), + columnCount: 2, + }, + }; + + public FetchResults(req: TFetchResultsReq, callback?: ThriftClientCommandCallback) { + this.fetchResultsReq = req; + callback?.(undefined, this.fetchResultsResp); + } + + public getDelegationTokenReq?: TGetDelegationTokenReq; + + public getDelegationTokenResp: TGetDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + delegationToken: 'token', + }; + + public GetDelegationToken( + req: TGetDelegationTokenReq, + callback?: ThriftClientCommandCallback, + ) { + this.getDelegationTokenReq = req; + callback?.(undefined, this.getDelegationTokenResp); + } + + public cancelDelegationTokenReq?: TCancelDelegationTokenReq; + + public cancelDelegationTokenResp: TCancelDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public CancelDelegationToken( + req: TCancelDelegationTokenReq, + callback?: ThriftClientCommandCallback, + ) { + this.cancelDelegationTokenReq = req; + callback?.(undefined, this.cancelDelegationTokenResp); + } + + public renewDelegationTokenReq?: TRenewDelegationTokenReq; + + public renewDelegationTokenResp: TRenewDelegationTokenResp = { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }; + + public RenewDelegationToken( + req: TRenewDelegationTokenReq, + callback?: ThriftClientCommandCallback, + ) { + this.renewDelegationTokenReq = req; + callback?.(undefined, this.renewDelegationTokenResp); + } +} diff --git a/tests/unit/DBSQLClient.test.js b/tests/unit/DBSQLClient.test.js deleted file mode 100644 index 3f149366..00000000 --- a/tests/unit/DBSQLClient.test.js +++ /dev/null @@ -1,445 +0,0 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const DBSQLClient = require('../../lib/DBSQLClient').default; -const DBSQLSession = require('../../lib/DBSQLSession').default; - -const PlainHttpAuthentication = require('../../lib/connection/auth/PlainHttpAuthentication').default; -const DatabricksOAuth = require('../../lib/connection/auth/DatabricksOAuth').default; -const { DatabricksOAuthManager, AzureOAuthManager } = require('../../lib/connection/auth/DatabricksOAuth/OAuthManager'); - -const HttpConnectionModule = require('../../lib/connection/connections/HttpConnection'); - -const { default: HttpConnection } = HttpConnectionModule; - -class AuthProviderMock { - constructor() { - this.authResult = {}; - } - - authenticate() { - return Promise.resolve(this.authResult); - } -} - -describe('DBSQLClient.connect', () => { - const options = { - host: '127.0.0.1', - path: '', - token: 'dapi********************************', - }; - - afterEach(() => { - HttpConnectionModule.default.restore?.(); - }); - - it('should prepend "/" to path if it is missing', async () => { - const client = new DBSQLClient(); - - const path = 'example/path'; - const connectionOptions = client.getConnectionOptions({ ...options, path }, {}); - - expect(connectionOptions.path).to.equal(`/${path}`); - }); - - it('should not prepend "/" to path if it is already available', async () => { - const client = new DBSQLClient(); - - const path = '/example/path'; - const connectionOptions = client.getConnectionOptions({ ...options, path }, {}); - - expect(connectionOptions.path).to.equal(path); - }); - - it('should initialize connection state', async () => { - const client = new DBSQLClient(); - - expect(client.client).to.be.undefined; - expect(client.authProvider).to.be.undefined; - expect(client.connectionProvider).to.be.undefined; - - await client.connect(options); - - expect(client.client).to.be.undefined; // it should not be initialized at this point - expect(client.authProvider).to.be.instanceOf(PlainHttpAuthentication); - expect(client.connectionProvider).to.be.instanceOf(HttpConnection); - }); - - it('should listen for Thrift connection events', async () => { - const client = new DBSQLClient(); - - const thriftConnectionMock = { - on: sinon.stub(), - }; - - sinon.stub(HttpConnectionModule, 'default').returns({ - getThriftConnection: () => Promise.resolve(thriftConnectionMock), - }); - - await client.connect(options); - - expect(thriftConnectionMock.on.called).to.be.true; - }); -}); - -describe('DBSQLClient.openSession', () => { - it('should successfully open session', async () => { - const client = new DBSQLClient(); - - sinon.stub(client, 'getClient').returns( - Promise.resolve({ - OpenSession(req, cb) { - cb(null, { status: {}, sessionHandle: {} }); - }, - }), - ); - - client.authProvider = {}; - client.connectionOptions = {}; - - const session = await client.openSession(); - expect(session).instanceOf(DBSQLSession); - }); - - it('should use initial namespace options', async () => { - const client = new DBSQLClient(); - - sinon.stub(client, 'getClient').returns( - Promise.resolve({ - OpenSession(req, cb) { - cb(null, { status: {}, sessionHandle: {} }); - }, - }), - ); - - client.authProvider = {}; - client.connectionOptions = {}; - - case1: { - const session = await client.openSession({ initialCatalog: 'catalog' }); - expect(session).instanceOf(DBSQLSession); - } - - case2: { - const session = await client.openSession({ initialSchema: 'schema' }); - expect(session).instanceOf(DBSQLSession); - } - - case3: { - const session = await client.openSession({ initialCatalog: 'catalog', initialSchema: 'schema' }); - expect(session).instanceOf(DBSQLSession); - } - }); - - it('should throw an exception when not connected', async () => { - const client = new DBSQLClient(); - client.connection = null; - - try { - await client.openSession(); - expect.fail('It should throw an error'); - } catch (error) { - expect(error.message).to.be.eq('DBSQLClient: not connected'); - } - }); - - it('should throw an exception when the connection is lost', async () => { - const client = new DBSQLClient(); - client.connection = { - isConnected() { - return false; - }, - }; - - try { - await client.openSession(); - expect.fail('It should throw an error'); - } catch (error) { - expect(error.message).to.be.eq('DBSQLClient: not connected'); - } - }); -}); - -describe('DBSQLClient.getClient', () => { - const options = { - host: '127.0.0.1', - path: '', - token: 'dapi********************************', - }; - - it('should throw an error if not connected', async () => { - const client = new DBSQLClient(); - try { - await client.getClient(); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(error.message).to.contain('DBSQLClient: not connected'); - } - }); - - it("should create client if wasn't not initialized yet", async () => { - const client = new DBSQLClient(); - - const thriftClient = {}; - - client.authProvider = new AuthProviderMock(); - client.connectionProvider = new HttpConnection({ ...options }, client); - client.thrift = { - createClient: sinon.stub().returns(thriftClient), - }; - - const result = await client.getClient(); - expect(client.thrift.createClient.called).to.be.true; - expect(result).to.be.equal(thriftClient); - }); - - it('should update auth credentials each time when client is requested', async () => { - const client = new DBSQLClient(); - - const thriftClient = {}; - - client.connectionProvider = new HttpConnection({ ...options }, client); - client.thrift = { - createClient: sinon.stub().returns(thriftClient), - }; - - sinon.stub(client.connectionProvider, 'setHeaders').callThrough(); - - // just a sanity check - authProvider should be initialized by this time, but if not it should not be used - expect(client.connectionProvider.setHeaders.callCount).to.be.equal(0); - await client.getClient(); - expect(client.connectionProvider.setHeaders.callCount).to.be.equal(0); - - client.authProvider = new AuthProviderMock(); - - // initialize client - firstCall: { - const result = await client.getClient(); - expect(client.thrift.createClient.callCount).to.be.equal(1); - expect(client.connectionProvider.setHeaders.callCount).to.be.equal(1); - expect(result).to.be.equal(thriftClient); - } - - // credentials stay the same, client should not be re-created - secondCall: { - const result = await client.getClient(); - expect(client.thrift.createClient.callCount).to.be.equal(1); - expect(client.connectionProvider.setHeaders.callCount).to.be.equal(2); - expect(result).to.be.equal(thriftClient); - } - - // change credentials mock - client should be re-created - thirdCall: { - client.authProvider.authResult = { b: 2 }; - - const result = await client.getClient(); - expect(client.thrift.createClient.callCount).to.be.equal(1); - expect(client.connectionProvider.setHeaders.callCount).to.be.equal(3); - expect(result).to.be.equal(thriftClient); - } - }); -}); - -describe('DBSQLClient.close', () => { - it('should close the connection if it was initiated', async () => { - const client = new DBSQLClient(); - client.client = {}; - client.connectionProvider = {}; - client.authProvider = {}; - - await client.close(); - expect(client.client).to.be.undefined; - expect(client.connectionProvider).to.be.undefined; - expect(client.authProvider).to.be.undefined; - // No additional asserts needed - it should just reach this point - }); - - it('should do nothing if the connection does not exist', async () => { - const client = new DBSQLClient(); - - await client.close(); - expect(client.client).to.be.undefined; - expect(client.connectionProvider).to.be.undefined; - expect(client.authProvider).to.be.undefined; - // No additional asserts needed - it should just reach this point - }); - - it('should close sessions that belong to it', async () => { - const client = new DBSQLClient(); - - const thriftClientMock = { - OpenSession(req, cb) { - cb(null, { - status: {}, - sessionHandle: { - sessionId: { - guid: Buffer.alloc(16), - secret: Buffer.alloc(0), - }, - }, - }); - }, - CloseSession(req, cb) { - cb(null, { status: {} }); - }, - }; - client.client = thriftClientMock; - sinon.stub(client, 'getClient').returns(Promise.resolve(thriftClientMock)); - - const session = await client.openSession(); - expect(session.onClose).to.be.not.undefined; - expect(session.isOpen).to.be.true; - expect(client.sessions.items.size).to.eq(1); - - sinon.spy(thriftClientMock, 'CloseSession'); - sinon.spy(client.sessions, 'closeAll'); - sinon.spy(session, 'close'); - - await client.close(); - expect(client.sessions.closeAll.called).to.be.true; - expect(session.close.called).to.be.true; - expect(session.onClose).to.be.undefined; - expect(session.isOpen).to.be.false; - expect(client.sessions.items.size).to.eq(0); - expect(thriftClientMock.CloseSession.called).to.be.true; - }); -}); - -describe('DBSQLClient.initAuthProvider', () => { - it('should use access token auth method', () => { - const client = new DBSQLClient(); - - const testAccessToken = 'token'; - const provider = client.initAuthProvider({ - authType: 'access-token', - token: testAccessToken, - }); - - expect(provider).to.be.instanceOf(PlainHttpAuthentication); - expect(provider.password).to.be.equal(testAccessToken); - }); - - it('should use access token auth method by default (compatibility)', () => { - const client = new DBSQLClient(); - - const testAccessToken = 'token'; - const provider = client.initAuthProvider({ - // note: no `authType` provided - token: testAccessToken, - }); - - expect(provider).to.be.instanceOf(PlainHttpAuthentication); - expect(provider.password).to.be.equal(testAccessToken); - }); - - it('should use Databricks OAuth method (AWS)', () => { - const client = new DBSQLClient(); - - const provider = client.initAuthProvider({ - authType: 'databricks-oauth', - // host is used when creating OAuth manager, so make it look like a real AWS instance - host: 'example.dev.databricks.com', - oauthClientSecret: 'test-secret', - }); - - expect(provider).to.be.instanceOf(DatabricksOAuth); - expect(provider.manager).to.be.instanceOf(DatabricksOAuthManager); - }); - - it('should use Databricks OAuth method (Azure)', () => { - const client = new DBSQLClient(); - - const provider = client.initAuthProvider({ - authType: 'databricks-oauth', - // host is used when creating OAuth manager, so make it look like a real Azure instance - host: 'example.databricks.azure.us', - }); - - expect(provider).to.be.instanceOf(DatabricksOAuth); - expect(provider.manager).to.be.instanceOf(AzureOAuthManager); - }); - - it('should use Databricks OAuth method (GCP)', () => { - const client = new DBSQLClient(); - - const provider = client.initAuthProvider({ - authType: 'databricks-oauth', - // host is used when creating OAuth manager, so make it look like a real AWS instance - host: 'example.gcp.databricks.com', - }); - - expect(provider).to.be.instanceOf(DatabricksOAuth); - expect(provider.manager).to.be.instanceOf(DatabricksOAuthManager); - }); - - it('should use Databricks InHouse OAuth method (Azure)', () => { - const client = new DBSQLClient(); - - // When `useDatabricksOAuthInAzure = true`, it should use Databricks OAuth method - // only for supported Azure hosts, and fail for others - - case1: { - const provider = client.initAuthProvider({ - authType: 'databricks-oauth', - // host is used when creating OAuth manager, so make it look like a real Azure instance - host: 'example.azuredatabricks.net', - useDatabricksOAuthInAzure: true, - }); - - expect(provider).to.be.instanceOf(DatabricksOAuth); - expect(provider.manager).to.be.instanceOf(DatabricksOAuthManager); - } - - case2: { - expect(() => { - const provider = client.initAuthProvider({ - authType: 'databricks-oauth', - // host is used when creating OAuth manager, so make it look like a real Azure instance - host: 'example.databricks.azure.us', - useDatabricksOAuthInAzure: true, - }); - }).to.throw(); - } - }); - - it('should throw error when OAuth not supported for host', () => { - const client = new DBSQLClient(); - - expect(() => { - client.initAuthProvider({ - authType: 'databricks-oauth', - // use host which is not supported for sure - host: 'example.com', - }); - }).to.throw(); - }); - - it('should use custom auth method', () => { - const client = new DBSQLClient(); - - const customProvider = {}; - - const provider = client.initAuthProvider({ - authType: 'custom', - provider: customProvider, - }); - - expect(provider).to.be.equal(customProvider); - }); - - it('should use custom auth method (legacy way)', () => { - const client = new DBSQLClient(); - - const customProvider = {}; - - const provider = client.initAuthProvider( - // custom provider from second arg should be used no matter what's specified in config - { authType: 'access-token', token: 'token' }, - customProvider, - ); - - expect(provider).to.be.equal(customProvider); - }); -}); diff --git a/tests/unit/DBSQLClient.test.ts b/tests/unit/DBSQLClient.test.ts new file mode 100644 index 00000000..5caf8420 --- /dev/null +++ b/tests/unit/DBSQLClient.test.ts @@ -0,0 +1,454 @@ +import { expect, AssertionError } from 'chai'; +import sinon from 'sinon'; +import DBSQLClient, { ThriftLibrary } from '../../lib/DBSQLClient'; +import DBSQLSession from '../../lib/DBSQLSession'; + +import PlainHttpAuthentication from '../../lib/connection/auth/PlainHttpAuthentication'; +import DatabricksOAuth from '../../lib/connection/auth/DatabricksOAuth'; +import { DatabricksOAuthManager, AzureOAuthManager } from '../../lib/connection/auth/DatabricksOAuth/OAuthManager'; + +import HttpConnection from '../../lib/connection/connections/HttpConnection'; +import { ConnectionOptions } from '../../lib/contracts/IDBSQLClient'; +import IRetryPolicy from '../../lib/connection/contracts/IRetryPolicy'; +import IConnectionProvider, { HttpTransactionDetails } from '../../lib/connection/contracts/IConnectionProvider'; +import ThriftClientStub from './.stubs/ThriftClientStub'; +import IThriftClient from '../../lib/contracts/IThriftClient'; +import IAuthentication from '../../lib/connection/contracts/IAuthentication'; +import AuthProviderStub from './.stubs/AuthProviderStub'; +import ConnectionProviderStub from './.stubs/ConnectionProviderStub'; + +const connectOptions = { + host: '127.0.0.1', + port: 80, + path: '', + token: 'dapi********************************', +} satisfies ConnectionOptions; + +describe('DBSQLClient.connect', () => { + it('should prepend "/" to path if it is missing', async () => { + const client = new DBSQLClient(); + + const path = 'example/path'; + const connectionOptions = client['getConnectionOptions']({ ...connectOptions, path }); + + expect(connectionOptions.path).to.equal(`/${path}`); + }); + + it('should not prepend "/" to path if it is already available', async () => { + const client = new DBSQLClient(); + + const path = '/example/path'; + const connectionOptions = client['getConnectionOptions']({ ...connectOptions, path }); + + expect(connectionOptions.path).to.equal(path); + }); + + it('should initialize connection state', async () => { + const client = new DBSQLClient(); + + expect(client['client']).to.be.undefined; + expect(client['authProvider']).to.be.undefined; + expect(client['connectionProvider']).to.be.undefined; + + await client.connect(connectOptions); + + expect(client['client']).to.be.undefined; // it should not be initialized at this point + expect(client['authProvider']).to.be.instanceOf(PlainHttpAuthentication); + expect(client['connectionProvider']).to.be.instanceOf(HttpConnection); + }); + + it('should listen for Thrift connection events', async () => { + const client = new DBSQLClient(); + + const thriftConnectionStub = { + on: sinon.stub(), + }; + + // This method is private, so we cannot easily `sinon.stub` it. + // But in this case we can just replace it + client['createConnectionProvider'] = () => ({ + getThriftConnection: async () => thriftConnectionStub, + getAgent: async () => undefined, + setHeaders: () => {}, + getRetryPolicy: (): Promise> => { + throw new Error('Not implemented'); + }, + }); + + await client.connect(connectOptions); + + expect(thriftConnectionStub.on.called).to.be.true; + }); +}); + +describe('DBSQLClient.openSession', () => { + it('should successfully open session', async () => { + const client = new DBSQLClient(); + const thriftClient = new ThriftClientStub(); + sinon.stub(client, 'getClient').returns(Promise.resolve(thriftClient)); + + const session = await client.openSession(); + expect(session).instanceOf(DBSQLSession); + }); + + it('should use initial namespace options', async () => { + const client = new DBSQLClient(); + const thriftClient = new ThriftClientStub(); + sinon.stub(client, 'getClient').returns(Promise.resolve(thriftClient)); + + case1: { + const initialCatalog = 'catalog1'; + const session = await client.openSession({ initialCatalog }); + expect(session).instanceOf(DBSQLSession); + expect(thriftClient.openSessionReq?.initialNamespace?.catalogName).to.equal(initialCatalog); + expect(thriftClient.openSessionReq?.initialNamespace?.schemaName).to.be.null; + } + + case2: { + const initialSchema = 'schema2'; + const session = await client.openSession({ initialSchema }); + expect(session).instanceOf(DBSQLSession); + expect(thriftClient.openSessionReq?.initialNamespace?.catalogName).to.be.null; + expect(thriftClient.openSessionReq?.initialNamespace?.schemaName).to.equal(initialSchema); + } + + case3: { + const initialCatalog = 'catalog3'; + const initialSchema = 'schema3'; + const session = await client.openSession({ initialCatalog, initialSchema }); + expect(session).instanceOf(DBSQLSession); + expect(thriftClient.openSessionReq?.initialNamespace?.catalogName).to.equal(initialCatalog); + expect(thriftClient.openSessionReq?.initialNamespace?.schemaName).to.equal(initialSchema); + } + }); + + it('should throw an exception when not connected', async () => { + const client = new DBSQLClient(); + client['connectionProvider'] = undefined; + + try { + await client.openSession(); + expect.fail('It should throw an error'); + } catch (error) { + if (!(error instanceof Error)) { + throw error; + } + expect(error.message).to.be.eq('DBSQLClient: not connected'); + } + }); +}); + +describe('DBSQLClient.getClient', () => { + it('should throw an error if not connected', async () => { + const client = new DBSQLClient(); + try { + await client.getClient(); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(error.message).to.contain('DBSQLClient: not connected'); + } + }); + + it('should create client if was not initialized yet', async () => { + const client = new DBSQLClient(); + + const thriftClient = new ThriftClientStub(); + const createThriftClient = sinon.stub().returns(thriftClient); + + client['authProvider'] = new AuthProviderStub(); + client['connectionProvider'] = new ConnectionProviderStub(); + client['thrift'] = { + createClient: createThriftClient, + }; + + const result = await client.getClient(); + expect(createThriftClient.called).to.be.true; + expect(result).to.be.equal(thriftClient); + }); + + it('should update auth credentials each time when client is requested', async () => { + const client = new DBSQLClient(); + + const thriftClient = new ThriftClientStub(); + const createThriftClient = sinon.stub().returns(thriftClient); + const authProvider = sinon.spy(new AuthProviderStub()); + const connectionProvider = sinon.spy(new ConnectionProviderStub()); + + client['connectionProvider'] = connectionProvider; + client['thrift'] = { + createClient: createThriftClient, + }; + + // just a sanity check - authProvider should not be initialized until `getClient()` call + expect(client['authProvider']).to.be.undefined; + expect(connectionProvider.setHeaders.callCount).to.be.equal(0); + await client.getClient(); + expect(authProvider.authenticate.callCount).to.be.equal(0); + expect(connectionProvider.setHeaders.callCount).to.be.equal(0); + + client['authProvider'] = authProvider; + + // initialize client + firstCall: { + const result = await client.getClient(); + expect(createThriftClient.callCount).to.be.equal(1); + expect(connectionProvider.setHeaders.callCount).to.be.equal(1); + expect(result).to.be.equal(thriftClient); + } + + // credentials stay the same, client should not be re-created + secondCall: { + const result = await client.getClient(); + expect(createThriftClient.callCount).to.be.equal(1); + expect(connectionProvider.setHeaders.callCount).to.be.equal(2); + expect(result).to.be.equal(thriftClient); + } + + // change credentials stub - client should be re-created + thirdCall: { + authProvider.headers = { test: 'test' }; + + const result = await client.getClient(); + expect(createThriftClient.callCount).to.be.equal(1); + expect(connectionProvider.setHeaders.callCount).to.be.equal(3); + expect(result).to.be.equal(thriftClient); + } + }); +}); + +describe('DBSQLClient.close', () => { + it('should close the connection if it was initiated', async () => { + const client = new DBSQLClient(); + client['client'] = new ThriftClientStub(); + client['connectionProvider'] = new ConnectionProviderStub(); + client['authProvider'] = new AuthProviderStub(); + + await client.close(); + expect(client['client']).to.be.undefined; + expect(client['connectionProvider']).to.be.undefined; + expect(client['authProvider']).to.be.undefined; + }); + + it('should do nothing if the connection does not exist', async () => { + const client = new DBSQLClient(); + + expect(client['client']).to.be.undefined; + expect(client['connectionProvider']).to.be.undefined; + expect(client['authProvider']).to.be.undefined; + + await client.close(); + expect(client['client']).to.be.undefined; + expect(client['connectionProvider']).to.be.undefined; + expect(client['authProvider']).to.be.undefined; + }); + + it('should close sessions that belong to it', async () => { + const client = new DBSQLClient(); + const thriftClient = sinon.spy(new ThriftClientStub()); + + client['client'] = thriftClient; + client['connectionProvider'] = new ConnectionProviderStub(); + client['authProvider'] = new AuthProviderStub(); + + const session = await client.openSession(); + if (!(session instanceof DBSQLSession)) { + throw new Error('Assertion error: expected session to be DBSQLSession'); + } + + expect(session.onClose).to.be.not.undefined; + expect(session['isOpen']).to.be.true; + expect(client['sessions']['items'].size).to.eq(1); + + const closeAllSessionsSpy = sinon.spy(client['sessions'], 'closeAll'); + const sessionCloseSpy = sinon.spy(session, 'close'); + + await client.close(); + expect(closeAllSessionsSpy.called).to.be.true; + expect(sessionCloseSpy.called).to.be.true; + expect(session.onClose).to.be.undefined; + expect(session['isOpen']).to.be.false; + expect(client['sessions']['items'].size).to.eq(0); + expect(thriftClient.CloseSession.called).to.be.true; + }); +}); + +describe('DBSQLClient.createAuthProvider', () => { + it('should use access token auth method', () => { + const client = new DBSQLClient(); + + const testAccessToken = 'token'; + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'access-token', + token: testAccessToken, + }); + + expect(provider).to.be.instanceOf(PlainHttpAuthentication); + if (!(provider instanceof PlainHttpAuthentication)) { + throw new Error('Assertion error: expected provider to be PlainHttpAuthentication'); + } + expect(provider['password']).to.be.equal(testAccessToken); + }); + + it('should use access token auth method by default (compatibility)', () => { + const client = new DBSQLClient(); + + const testAccessToken = 'token'; + const provider = client['createAuthProvider']({ + ...connectOptions, + // note: no `authType` provided + token: testAccessToken, + }); + + expect(provider).to.be.instanceOf(PlainHttpAuthentication); + if (!(provider instanceof PlainHttpAuthentication)) { + throw new Error('Assertion error: expected provider to be PlainHttpAuthentication'); + } + expect(provider['password']).to.be.equal(testAccessToken); + }); + + it('should use Databricks OAuth method (AWS)', () => { + const client = new DBSQLClient(); + + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // host is used when creating OAuth manager, so make it look like a real AWS instance + host: 'example.dev.databricks.com', + oauthClientSecret: 'test-secret', + }); + + expect(provider).to.be.instanceOf(DatabricksOAuth); + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Assertion error: expected provider to be DatabricksOAuth'); + } + expect(provider['getManager']()).to.be.instanceOf(DatabricksOAuthManager); + }); + + it('should use Databricks OAuth method (Azure)', () => { + const client = new DBSQLClient(); + + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // host is used when creating OAuth manager, so make it look like a real Azure instance + host: 'example.databricks.azure.us', + }); + + expect(provider).to.be.instanceOf(DatabricksOAuth); + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Assertion error: expected provider to be DatabricksOAuth'); + } + expect(provider['getManager']()).to.be.instanceOf(AzureOAuthManager); + }); + + it('should use Databricks OAuth method (GCP)', () => { + const client = new DBSQLClient(); + + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // host is used when creating OAuth manager, so make it look like a real AWS instance + host: 'example.gcp.databricks.com', + }); + + expect(provider).to.be.instanceOf(DatabricksOAuth); + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Assertion error: expected provider to be DatabricksOAuth'); + } + expect(provider['getManager']()).to.be.instanceOf(DatabricksOAuthManager); + }); + + it('should use Databricks InHouse OAuth method (Azure)', () => { + const client = new DBSQLClient(); + + // When `useDatabricksOAuthInAzure = true`, it should use Databricks OAuth method + // only for supported Azure hosts, and fail for others + + case1: { + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // host is used when creating OAuth manager, so make it look like a real Azure instance + host: 'example.azuredatabricks.net', + useDatabricksOAuthInAzure: true, + }); + + expect(provider).to.be.instanceOf(DatabricksOAuth); + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Assertion error: expected provider to be DatabricksOAuth'); + } + expect(provider['getManager']()).to.be.instanceOf(DatabricksOAuthManager); + } + + case2: { + expect(() => { + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // host is used when creating OAuth manager, so make it look like a real Azure instance + host: 'example.databricks.azure.us', + useDatabricksOAuthInAzure: true, + }); + + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Expected `provider` to be `DatabricksOAuth`'); + } + provider['getManager'](); // just call the method + }).to.throw(); + } + }); + + it('should throw error when OAuth not supported for host', () => { + const client = new DBSQLClient(); + + expect(() => { + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'databricks-oauth', + // use host which is not supported for sure + host: 'example.com', + }); + + if (!(provider instanceof DatabricksOAuth)) { + throw new Error('Expected `provider` to be `DatabricksOAuth`'); + } + provider['getManager'](); // just call the method + }).to.throw(); + }); + + it('should use custom auth method', () => { + const client = new DBSQLClient(); + + const customProvider = { + authenticate: () => Promise.resolve({}), + }; + + const provider = client['createAuthProvider']({ + ...connectOptions, + authType: 'custom', + provider: customProvider, + }); + + expect(provider).to.be.equal(customProvider); + }); + + it('should use custom auth method (legacy way)', () => { + const client = new DBSQLClient(); + + const customProvider = { + authenticate: () => Promise.resolve({}), + }; + + const provider = client['createAuthProvider']( + // custom provider from second arg should be used no matter what's specified in config + { ...connectOptions, authType: 'access-token', token: 'token' }, + customProvider, + ); + + expect(provider).to.be.equal(customProvider); + }); +}); diff --git a/tests/unit/DBSQLOperation.test.js b/tests/unit/DBSQLOperation.test.js deleted file mode 100644 index 81e2be3a..00000000 --- a/tests/unit/DBSQLOperation.test.js +++ /dev/null @@ -1,1312 +0,0 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const { DBSQLLogger, LogLevel } = require('../../lib'); -const { TStatusCode, TOperationState, TTypeId, TSparkRowSetType } = require('../../thrift/TCLIService_types'); -const DBSQLClient = require('../../lib/DBSQLClient').default; -const DBSQLOperation = require('../../lib/DBSQLOperation').default; -const StatusError = require('../../lib/errors/StatusError').default; -const OperationStateError = require('../../lib/errors/OperationStateError').default; -const HiveDriverError = require('../../lib/errors/HiveDriverError').default; -const JsonResultHandler = require('../../lib/result/JsonResultHandler').default; -const ArrowResultConverter = require('../../lib/result/ArrowResultConverter').default; -const ArrowResultHandler = require('../../lib/result/ArrowResultHandler').default; -const CloudFetchResultHandler = require('../../lib/result/CloudFetchResultHandler').default; -const ResultSlicer = require('../../lib/result/ResultSlicer').default; - -class OperationHandleMock { - constructor(hasResultSet = true) { - this.operationId = 1; - this.hasResultSet = !!hasResultSet; - } -} - -async function expectFailure(fn) { - try { - await fn(); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - } -} - -class DriverMock { - getOperationStatusResp = { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - operationState: TOperationState.INITIALIZED_STATE, - hasResultSet: false, - }; - - getResultSetMetadataResp = { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - resultFormat: TSparkRowSetType.COLUMN_BASED_SET, - schema: { - columns: [ - { - columnName: 'test', - position: 1, - typeDesc: { - types: [ - { - primitiveEntry: { - type: TTypeId.INT_TYPE, - }, - }, - ], - }, - }, - ], - }, - }; - - fetchResultsResp = { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - hasMoreRows: false, - results: { - columns: [ - { - i32Val: { - values: [1, 2, 3], - }, - }, - ], - }, - }; - - cancelOperationResp = { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - }; - - closeOperationResp = { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - }; - - getOperationStatus() { - return Promise.resolve(this.getOperationStatusResp); - } - - getResultSetMetadata() { - return Promise.resolve(this.getResultSetMetadataResp); - } - - fetchResults() { - return Promise.resolve(this.fetchResultsResp); - } - - cancelOperation() { - return Promise.resolve(this.cancelOperationResp); - } - - closeOperation() { - return Promise.resolve(this.closeOperationResp); - } -} - -class ClientContextMock { - constructor(props) { - // Create logger that won't emit - this.logger = new DBSQLLogger({ level: LogLevel.error }); - this.driver = new DriverMock(); - } - - getConfig() { - return DBSQLClient.getDefaultConfig(); - } - - getLogger() { - return this.logger; - } - - async getDriver() { - return this.driver; - } -} - -describe('DBSQLOperation', () => { - describe('status', () => { - it('should pick up state from operation handle', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.state).to.equal(TOperationState.INITIALIZED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - }); - - it('should pick up state from directResults', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - operationStatus: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - operationState: TOperationState.FINISHED_STATE, - hasResultSet: true, - }, - }, - }); - - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - }); - - it('should fetch status and update internal state', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.state).to.equal(TOperationState.INITIALIZED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.false; - - const status = await operation.status(); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - }); - - it('should request progress', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - - const operation = new DBSQLOperation({ handle, context }); - await operation.status(true); - - expect(context.driver.getOperationStatus.called).to.be.true; - const request = context.driver.getOperationStatus.getCall(0).args[0]; - expect(request.getProgressUpdate).to.be.true; - }); - - it('should not fetch status once operation is finished', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.hasResultSet = true; - - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.state).to.equal(TOperationState.INITIALIZED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.false; - - // First call - should fetch data and cache - context.driver.getOperationStatusResp = { - ...context.driver.getOperationStatusResp, - operationState: TOperationState.FINISHED_STATE, - }; - const status1 = await operation.status(); - - expect(context.driver.getOperationStatus.callCount).to.equal(1); - expect(status1.operationState).to.equal(TOperationState.FINISHED_STATE); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - - // Second call - should return cached data - context.driver.getOperationStatusResp = { - ...context.driver.getOperationStatusResp, - operationState: TOperationState.RUNNING_STATE, - }; - const status2 = await operation.status(); - - expect(context.driver.getOperationStatus.callCount).to.equal(1); - expect(status2.operationState).to.equal(TOperationState.FINISHED_STATE); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - }); - - it('should fetch status if directResults status is not finished', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - operationStatus: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - operationState: TOperationState.RUNNING_STATE, - hasResultSet: false, - }, - }, - }); - - expect(operation.state).to.equal(TOperationState.RUNNING_STATE); // from directResults - expect(operation.operationHandle.hasResultSet).to.be.false; - - const status = await operation.status(false); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.true; - }); - - it('should not fetch status if directResults status is finished', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.operationState = TOperationState.RUNNING_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - operationStatus: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - operationState: TOperationState.FINISHED_STATE, - hasResultSet: false, - }, - }, - }); - - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); // from directResults - expect(operation.operationHandle.hasResultSet).to.be.false; - - const status = await operation.status(false); - - expect(context.driver.getOperationStatus.called).to.be.false; - expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - expect(operation.operationHandle.hasResultSet).to.be.false; - }); - - it('should throw an error in case of a status error', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.status.statusCode = TStatusCode.ERROR_STATUS; - const operation = new DBSQLOperation({ handle, context }); - - try { - await operation.status(false); - expect.fail('It should throw a StatusError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(StatusError); - } - }); - }); - - describe('cancel', () => { - it('should cancel operation and update state', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'cancelOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.cancel(); - - expect(context.driver.cancelOperation.called).to.be.true; - expect(operation.cancelled).to.be.true; - expect(operation.closed).to.be.false; - }); - - it('should return immediately if already cancelled', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'cancelOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.cancel(); - expect(context.driver.cancelOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.true; - expect(operation.closed).to.be.false; - - await operation.cancel(); - expect(context.driver.cancelOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.true; - expect(operation.closed).to.be.false; - }); - - it('should return immediately if already closed', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'cancelOperation'); - sinon.spy(context.driver, 'closeOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.close(); - expect(context.driver.closeOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - - await operation.cancel(); - expect(context.driver.cancelOperation.callCount).to.be.equal(0); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - }); - - it('should throw an error in case of a status error and keep state', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.cancelOperationResp.status.statusCode = TStatusCode.ERROR_STATUS; - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - try { - await operation.cancel(); - expect.fail('It should throw a StatusError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(StatusError); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - } - }); - - it('should reject all methods once cancelled', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - const operation = new DBSQLOperation({ handle, context }); - - await operation.cancel(); - expect(operation.cancelled).to.be.true; - - await expectFailure(() => operation.fetchAll()); - await expectFailure(() => operation.fetchChunk({ disableBuffering: true })); - await expectFailure(() => operation.status()); - await expectFailure(() => operation.finished()); - await expectFailure(() => operation.getSchema()); - }); - }); - - describe('close', () => { - it('should close operation and update state', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'closeOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.close(); - - expect(context.driver.closeOperation.called).to.be.true; - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - }); - - it('should return immediately if already closed', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'closeOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.close(); - expect(context.driver.closeOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - - await operation.close(); - expect(context.driver.closeOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - }); - - it('should return immediately if already cancelled', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'closeOperation'); - sinon.spy(context.driver, 'cancelOperation'); - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.cancel(); - expect(context.driver.cancelOperation.callCount).to.be.equal(1); - expect(operation.cancelled).to.be.true; - expect(operation.closed).to.be.false; - - await operation.close(); - expect(context.driver.closeOperation.callCount).to.be.equal(0); - expect(operation.cancelled).to.be.true; - expect(operation.closed).to.be.false; - }); - - it('should initialize from directResults', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'closeOperation'); - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - closeOperation: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - }, - }, - }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - await operation.close(); - - expect(context.driver.closeOperation.called).to.be.false; - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.true; - expect(context.driver.closeOperation.callCount).to.be.equal(0); - }); - - it('should throw an error in case of a status error and keep state', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.closeOperationResp.status.statusCode = TStatusCode.ERROR_STATUS; - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - - try { - await operation.close(); - expect.fail('It should throw a StatusError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(StatusError); - expect(operation.cancelled).to.be.false; - expect(operation.closed).to.be.false; - } - }); - - it('should reject all methods once closed', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - const operation = new DBSQLOperation({ handle, context }); - - await operation.close(); - expect(operation.closed).to.be.true; - - await expectFailure(() => operation.fetchAll()); - await expectFailure(() => operation.fetchChunk({ disableBuffering: true })); - await expectFailure(() => operation.status()); - await expectFailure(() => operation.finished()); - await expectFailure(() => operation.getSchema()); - }); - }); - - describe('finished', () => { - [TOperationState.INITIALIZED_STATE, TOperationState.RUNNING_STATE, TOperationState.PENDING_STATE].forEach( - (operationState) => { - it(`should wait for finished state starting from TOperationState.${TOperationState[operationState]}`, async () => { - const attemptsUntilFinished = 3; - - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = operationState; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onCall(attemptsUntilFinished - 1) // count is zero-based - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - const operation = new DBSQLOperation({ handle, context }); - - expect(operation.state).to.equal(TOperationState.INITIALIZED_STATE); - - await operation.finished(); - - expect(context.driver.getOperationStatus.callCount).to.be.equal(attemptsUntilFinished); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - }); - }, - ); - - it('should request progress', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onSecondCall() - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - const operation = new DBSQLOperation({ handle, context }); - await operation.finished({ progress: true }); - - expect(context.driver.getOperationStatus.called).to.be.true; - const request = context.driver.getOperationStatus.getCall(0).args[0]; - expect(request.getProgressUpdate).to.be.true; - }); - - it('should invoke progress callback', async () => { - const attemptsUntilFinished = 3; - - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onCall(attemptsUntilFinished - 1) // count is zero-based - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - const operation = new DBSQLOperation({ handle, context }); - - const callback = sinon.stub(); - - await operation.finished({ callback }); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(callback.callCount).to.be.equal(attemptsUntilFinished); - }); - - it('should pick up finished state from directResults', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - sinon.spy(context.driver, 'getOperationStatus'); - context.driver.getOperationStatusResp.status.statusCode = TStatusCode.SUCCESS_STATUS; - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - operationStatus: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - operationState: TOperationState.FINISHED_STATE, - hasResultSet: true, - }, - }, - }); - - await operation.finished(); - - // Once operation is finished - no need to fetch status again - expect(context.driver.getOperationStatus.called).to.be.false; - }); - - it('should throw an error in case of a status error', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.status.statusCode = TStatusCode.ERROR_STATUS; - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - const operation = new DBSQLOperation({ handle, context }); - - try { - await operation.finished(); - expect.fail('It should throw a StatusError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(StatusError); - } - }); - - [ - TOperationState.CANCELED_STATE, - TOperationState.CLOSED_STATE, - TOperationState.ERROR_STATE, - TOperationState.UKNOWN_STATE, - TOperationState.TIMEDOUT_STATE, - ].forEach((operationState) => { - it(`should throw an error in case of a TOperationState.${TOperationState[operationState]}`, async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.status.statusCode = TStatusCode.SUCCESS_STATUS; - context.driver.getOperationStatusResp.operationState = operationState; - const operation = new DBSQLOperation({ handle, context }); - - try { - await operation.finished(); - expect.fail('It should throw a OperationStateError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(OperationStateError); - } - }); - }); - }); - - describe('getSchema', () => { - it('should return immediately if operation has no results', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = false; - sinon.spy(context.driver, 'getResultSetMetadata'); - const operation = new DBSQLOperation({ handle, context }); - - const schema = await operation.getSchema(); - - expect(schema).to.be.null; - expect(context.driver.getResultSetMetadata.called).to.be.false; - }); - - it('should wait for operation to complete', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onSecondCall() - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - context.driver.getResultSetMetadataResp.schema = { columns: [] }; - - const operation = new DBSQLOperation({ handle, context }); - - const schema = await operation.getSchema(); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(schema).to.deep.equal(context.driver.getResultSetMetadataResp.schema); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - }); - - it('should request progress', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onSecondCall() - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - const operation = new DBSQLOperation({ handle, context }); - await operation.getSchema({ progress: true }); - - expect(context.driver.getOperationStatus.called).to.be.true; - const request = context.driver.getOperationStatus.getCall(0).args[0]; - expect(request.getProgressUpdate).to.be.true; - }); - - it('should invoke progress callback', async () => { - const attemptsUntilFinished = 3; - - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onCall(attemptsUntilFinished - 1) // count is zero-based - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - const operation = new DBSQLOperation({ handle, context }); - - const callback = sinon.stub(); - - await operation.getSchema({ callback }); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(callback.callCount).to.be.equal(attemptsUntilFinished); - }); - - it('should fetch schema if operation has data', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - const operation = new DBSQLOperation({ handle, context }); - - const schema = await operation.getSchema(); - - expect(schema).to.deep.equal(context.driver.getResultSetMetadataResp.schema); - expect(context.driver.getResultSetMetadata.called).to.be.true; - }); - - it('should return cached schema on subsequent calls', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - const operation = new DBSQLOperation({ handle, context }); - - const schema1 = await operation.getSchema(); - expect(schema1).to.deep.equal(context.driver.getResultSetMetadataResp.schema); - expect(context.driver.getResultSetMetadata.callCount).to.equal(1); - - const schema2 = await operation.getSchema(); - expect(schema2).to.deep.equal(context.driver.getResultSetMetadataResp.schema); - expect(context.driver.getResultSetMetadata.callCount).to.equal(1); // no additional requests - }); - - it('should use schema from directResults', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - - const directResults = { - resultSetMetadata: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - schema: { - columns: [{ columnName: 'another' }], - }, - }, - }; - const operation = new DBSQLOperation({ handle, context, directResults }); - - const schema = await operation.getSchema(); - - expect(schema).to.deep.equal(directResults.resultSetMetadata.schema); - expect(context.driver.getResultSetMetadata.called).to.be.false; - }); - - it('should throw an error in case of a status error', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.getResultSetMetadataResp.status.statusCode = TStatusCode.ERROR_STATUS; - const operation = new DBSQLOperation({ handle, context }); - - try { - await operation.getSchema(); - expect.fail('It should throw a StatusError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(StatusError); - } - }); - - it('should use appropriate result handler', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - - jsonHandler: { - context.driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.COLUMN_BASED_SET; - context.driver.getResultSetMetadata.resetHistory(); - - const operation = new DBSQLOperation({ handle, context }); - const resultHandler = await operation.getResultHandler(); - expect(context.driver.getResultSetMetadata.called).to.be.true; - expect(resultHandler).to.be.instanceOf(ResultSlicer); - expect(resultHandler.source).to.be.instanceOf(JsonResultHandler); - } - - arrowHandler: { - context.driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.ARROW_BASED_SET; - context.driver.getResultSetMetadata.resetHistory(); - - const operation = new DBSQLOperation({ handle, context }); - const resultHandler = await operation.getResultHandler(); - expect(context.driver.getResultSetMetadata.called).to.be.true; - expect(resultHandler).to.be.instanceOf(ResultSlicer); - expect(resultHandler.source).to.be.instanceOf(ArrowResultConverter); - expect(resultHandler.source.source).to.be.instanceOf(ArrowResultHandler); - } - - cloudFetchHandler: { - context.driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.URL_BASED_SET; - context.driver.getResultSetMetadata.resetHistory(); - - const operation = new DBSQLOperation({ handle, context }); - const resultHandler = await operation.getResultHandler(); - expect(context.driver.getResultSetMetadata.called).to.be.true; - expect(resultHandler).to.be.instanceOf(ResultSlicer); - expect(resultHandler.source).to.be.instanceOf(ArrowResultConverter); - expect(resultHandler.source.source).to.be.instanceOf(CloudFetchResultHandler); - } - }); - }); - - describe('fetchChunk', () => { - it('should return immediately if operation has no results', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = false; - - sinon.spy(context.driver, 'getResultSetMetadata'); - sinon.spy(context.driver, 'fetchResults'); - const operation = new DBSQLOperation({ handle, context }); - - const results = await operation.fetchChunk({ disableBuffering: true }); - - expect(results).to.deep.equal([]); - expect(context.driver.getResultSetMetadata.called).to.be.false; - expect(context.driver.fetchResults.called).to.be.false; - }); - - it('should wait for operation to complete', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onSecondCall() - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - context.driver.getResultSetMetadataResp.schema = { columns: [] }; - context.driver.fetchResultsResp.hasMoreRows = false; - context.driver.fetchResultsResp.results.columns = []; - - const operation = new DBSQLOperation({ handle, context }); - - const results = await operation.fetchChunk({ disableBuffering: true }); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(results).to.deep.equal([]); - expect(operation.state).to.equal(TOperationState.FINISHED_STATE); - }); - - it('should request progress', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onSecondCall() - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - context.driver.getResultSetMetadataResp.schema = { columns: [] }; - context.driver.fetchResultsResp.hasMoreRows = false; - context.driver.fetchResultsResp.results.columns = []; - - const operation = new DBSQLOperation({ handle, context }); - await operation.fetchChunk({ progress: true, disableBuffering: true }); - - expect(context.driver.getOperationStatus.called).to.be.true; - const request = context.driver.getOperationStatus.getCall(0).args[0]; - expect(request.getProgressUpdate).to.be.true; - }); - - it('should invoke progress callback', async () => { - const attemptsUntilFinished = 3; - - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - - context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; - sinon - .stub(context.driver, 'getOperationStatus') - .callThrough() - .onCall(attemptsUntilFinished - 1) // count is zero-based - .callsFake((...args) => { - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - return context.driver.getOperationStatus.wrappedMethod.apply(context.driver, args); - }); - - context.driver.getResultSetMetadataResp.schema = { columns: [] }; - context.driver.fetchResultsResp.hasMoreRows = false; - context.driver.fetchResultsResp.results.columns = []; - - const operation = new DBSQLOperation({ handle, context }); - - const callback = sinon.stub(); - - await operation.fetchChunk({ callback, disableBuffering: true }); - - expect(context.driver.getOperationStatus.called).to.be.true; - expect(callback.callCount).to.be.equal(attemptsUntilFinished); - }); - - it('should fetch schema and data and return array of records', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - sinon.spy(context.driver, 'fetchResults'); - - const operation = new DBSQLOperation({ handle, context }); - - const results = await operation.fetchChunk({ disableBuffering: true }); - - expect(results).to.deep.equal([{ test: 1 }, { test: 2 }, { test: 3 }]); - expect(context.driver.getResultSetMetadata.called).to.be.true; - expect(context.driver.fetchResults.called).to.be.true; - }); - - it('should return data from directResults (all the data in directResults)', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - sinon.spy(context.driver, 'getResultSetMetadata'); - sinon.spy(context.driver, 'fetchResults'); - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - resultSet: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - hasMoreRows: false, - results: { - columns: [ - { - i32Val: { - values: [5, 6], - }, - }, - ], - }, - }, - }, - }); - - const results = await operation.fetchChunk({ disableBuffering: true }); - - expect(results).to.deep.equal([{ test: 5 }, { test: 6 }]); - expect(context.driver.getResultSetMetadata.called).to.be.true; - expect(context.driver.fetchResults.called).to.be.false; - }); - - it('should return data from directResults (first chunk in directResults, next chunk fetched)', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - sinon.spy(context.driver, 'getResultSetMetadata'); - sinon.spy(context.driver, 'fetchResults'); - - const operation = new DBSQLOperation({ - handle, - context, - directResults: { - resultSet: { - status: { statusCode: TStatusCode.SUCCESS_STATUS }, - hasMoreRows: true, - results: { - columns: [ - { - i32Val: { - values: [5, 6], - }, - }, - ], - }, - }, - }, - }); - - const results1 = await operation.fetchChunk({ disableBuffering: true }); - - expect(results1).to.deep.equal([{ test: 5 }, { test: 6 }]); - expect(context.driver.getResultSetMetadata.callCount).to.be.eq(1); - expect(context.driver.fetchResults.callCount).to.be.eq(0); - - const results2 = await operation.fetchChunk({ disableBuffering: true }); - - expect(results2).to.deep.equal([{ test: 1 }, { test: 2 }, { test: 3 }]); - expect(context.driver.getResultSetMetadata.callCount).to.be.eq(1); - expect(context.driver.fetchResults.callCount).to.be.eq(1); - }); - - it('should fail on unsupported result format', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - - context.driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.ROW_BASED_SET; - context.driver.getResultSetMetadataResp.schema = { columns: [] }; - - const operation = new DBSQLOperation({ handle, context }); - - try { - await operation.fetchChunk({ disableBuffering: true }); - expect.fail('It should throw a HiveDriverError'); - } catch (e) { - if (e instanceof AssertionError) { - throw e; - } - expect(e).to.be.instanceOf(HiveDriverError); - } - }); - }); - - describe('fetchAll', () => { - it('should fetch data while available and return it all', async () => { - const context = new ClientContextMock(); - const handle = new OperationHandleMock(); - const operation = new DBSQLOperation({ handle, context }); - - const originalData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; - - const tempData = [...originalData]; - sinon.stub(operation, 'fetchChunk').callsFake(() => { - return Promise.resolve(tempData.splice(0, 3)); - }); - sinon.stub(operation, 'hasMoreRows').callsFake(() => { - return tempData.length > 0; - }); - - const fetchedData = await operation.fetchAll(); - - // Warning: this check is implementation-specific - // `fetchAll` should wait for operation to complete. In current implementation - // it does so by calling `fetchChunk` at least once, which internally does - // all the job. But since here we mock `fetchChunk` it won't really wait, - // therefore here we ensure it was called at least once - expect(operation.fetchChunk.callCount).to.be.gte(1); - - expect(operation.fetchChunk.called).to.be.true; - expect(operation.hasMoreRows.called).to.be.true; - expect(fetchedData).to.deep.equal(originalData); - }); - }); - - describe('hasMoreRows', () => { - it('should return initial value prior to first fetch', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = false; - context.driver.fetchResultsResp.results = undefined; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.undefined; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.false; - expect(operation._data.hasMoreRowsFlag).to.be.false; - }); - - it('should return False if operation was closed', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = true; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.true; - await operation.close(); - expect(await operation.hasMoreRows()).to.be.false; - }); - - it('should return False if operation was cancelled', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = true; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.true; - await operation.cancel(); - expect(await operation.hasMoreRows()).to.be.false; - }); - - it('should return True if hasMoreRows flag was set in response', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = true; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.undefined; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.true; - }); - - it('should return True if hasMoreRows flag is False but there is actual data', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = false; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.undefined; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.true; - }); - - it('should return True if hasMoreRows flag is unset but there is actual data', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = undefined; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.undefined; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.true; - }); - - it('should return False if hasMoreRows flag is False and there is no data', async () => { - const context = new ClientContextMock(); - - const handle = new OperationHandleMock(); - handle.hasResultSet = true; - - context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; - context.driver.getOperationStatusResp.hasResultSet = true; - context.driver.fetchResultsResp.hasMoreRows = false; - context.driver.fetchResultsResp.results = undefined; - const operation = new DBSQLOperation({ handle, context }); - - expect(await operation.hasMoreRows()).to.be.true; - expect(operation._data.hasMoreRowsFlag).to.be.undefined; - await operation.fetchChunk({ disableBuffering: true }); - expect(await operation.hasMoreRows()).to.be.false; - expect(operation._data.hasMoreRowsFlag).to.be.false; - }); - }); -}); diff --git a/tests/unit/DBSQLOperation.test.ts b/tests/unit/DBSQLOperation.test.ts new file mode 100644 index 00000000..94224455 --- /dev/null +++ b/tests/unit/DBSQLOperation.test.ts @@ -0,0 +1,1141 @@ +import { AssertionError, expect } from 'chai'; +import sinon from 'sinon'; +import Int64 from 'node-int64'; +import { + TOperationHandle, + TOperationState, + TOperationType, + TSparkDirectResults, + TSparkRowSetType, + TStatusCode, + TTypeId, +} from '../../thrift/TCLIService_types'; +import DBSQLOperation from '../../lib/DBSQLOperation'; +import StatusError from '../../lib/errors/StatusError'; +import OperationStateError from '../../lib/errors/OperationStateError'; +import HiveDriverError from '../../lib/errors/HiveDriverError'; +import JsonResultHandler from '../../lib/result/JsonResultHandler'; +import ArrowResultConverter from '../../lib/result/ArrowResultConverter'; +import ArrowResultHandler from '../../lib/result/ArrowResultHandler'; +import CloudFetchResultHandler from '../../lib/result/CloudFetchResultHandler'; +import ResultSlicer from '../../lib/result/ResultSlicer'; + +import ClientContextStub from './.stubs/ClientContextStub'; +import { Type } from 'apache-arrow'; + +function operationHandleStub(overrides: Partial): TOperationHandle { + return { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: true, + ...overrides, + }; +} + +async function expectFailure(fn: () => Promise) { + try { + await fn(); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError) { + throw error; + } + } +} + +describe('DBSQLOperation', () => { + describe('status', () => { + it('should pick up state from operation handle', async () => { + const context = new ClientContextStub(); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['state']).to.equal(TOperationState.INITIALIZED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + }); + + it('should pick up state from directResults', async () => { + const context = new ClientContextStub(); + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: { + operationStatus: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + hasResultSet: true, + }, + }, + }); + + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + }); + + it('should fetch status and update internal state', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: false }), context }); + + expect(operation['state']).to.equal(TOperationState.INITIALIZED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.false; + + const status = await operation.status(); + + expect(driver.getOperationStatus.called).to.be.true; + expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + }); + + it('should request progress', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: false }), context }); + await operation.status(true); + + expect(driver.getOperationStatus.called).to.be.true; + const request = driver.getOperationStatus.getCall(0).args[0]; + expect(request.getProgressUpdate).to.be.true; + }); + + it('should not fetch status once operation is finished', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: false }), context }); + + expect(operation['state']).to.equal(TOperationState.INITIALIZED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.false; + + // First call - should fetch data and cache + driver.getOperationStatusResp = { + ...driver.getOperationStatusResp, + operationState: TOperationState.FINISHED_STATE, + }; + const status1 = await operation.status(); + + expect(driver.getOperationStatus.callCount).to.equal(1); + expect(status1.operationState).to.equal(TOperationState.FINISHED_STATE); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + + // Second call - should return cached data + driver.getOperationStatusResp = { + ...driver.getOperationStatusResp, + operationState: TOperationState.RUNNING_STATE, + }; + const status2 = await operation.status(); + + expect(driver.getOperationStatus.callCount).to.equal(1); + expect(status2.operationState).to.equal(TOperationState.FINISHED_STATE); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + }); + + it('should fetch status if directResults status is not finished', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: false }), + context, + directResults: { + operationStatus: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.RUNNING_STATE, + hasResultSet: false, + }, + }, + }); + + expect(operation['state']).to.equal(TOperationState.RUNNING_STATE); // from directResults + expect(operation['operationHandle'].hasResultSet).to.be.false; + + const status = await operation.status(false); + + expect(driver.getOperationStatus.called).to.be.true; + expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.true; + }); + + it('should not fetch status if directResults status is finished', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.RUNNING_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: false }), + context, + directResults: { + operationStatus: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + hasResultSet: false, + }, + }, + }); + + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); // from directResults + expect(operation['operationHandle'].hasResultSet).to.be.false; + + const status = await operation.status(false); + + expect(driver.getOperationStatus.called).to.be.false; + expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + expect(operation['operationHandle'].hasResultSet).to.be.false; + }); + + it('should throw an error in case of a status error', async () => { + const context = new ClientContextStub(); + context.driver.getOperationStatusResp.status.statusCode = TStatusCode.ERROR_STATUS; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + try { + await operation.status(false); + expect.fail('It should throw a StatusError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(StatusError); + } + }); + }); + + describe('cancel', () => { + it('should cancel operation and update state', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.cancel(); + + expect(driver.cancelOperation.called).to.be.true; + expect(operation['cancelled']).to.be.true; + expect(operation['closed']).to.be.false; + }); + + it('should return immediately if already cancelled', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.cancel(); + expect(driver.cancelOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.true; + expect(operation['closed']).to.be.false; + + await operation.cancel(); + expect(driver.cancelOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.true; + expect(operation['closed']).to.be.false; + }); + + it('should return immediately if already closed', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.close(); + expect(driver.closeOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + + await operation.cancel(); + expect(driver.cancelOperation.callCount).to.be.equal(0); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + }); + + it('should throw an error in case of a status error and keep state', async () => { + const context = new ClientContextStub(); + context.driver.cancelOperationResp.status.statusCode = TStatusCode.ERROR_STATUS; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + try { + await operation.cancel(); + expect.fail('It should throw a StatusError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(StatusError); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + } + }); + + it('should reject all methods once cancelled', async () => { + const context = new ClientContextStub(); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + await operation.cancel(); + expect(operation['cancelled']).to.be.true; + + await expectFailure(() => operation.fetchAll()); + await expectFailure(() => operation.fetchChunk({ disableBuffering: true })); + await expectFailure(() => operation.status()); + await expectFailure(() => operation.finished()); + await expectFailure(() => operation.getSchema()); + }); + }); + + describe('close', () => { + it('should close operation and update state', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.close(); + + expect(driver.closeOperation.called).to.be.true; + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + }); + + it('should return immediately if already closed', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.close(); + expect(driver.closeOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + + await operation.close(); + expect(driver.closeOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + }); + + it('should return immediately if already cancelled', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.cancel(); + expect(driver.cancelOperation.callCount).to.be.equal(1); + expect(operation['cancelled']).to.be.true; + expect(operation['closed']).to.be.false; + + await operation.close(); + expect(driver.closeOperation.callCount).to.be.equal(0); + expect(operation['cancelled']).to.be.true; + expect(operation['closed']).to.be.false; + }); + + it('should initialize from directResults', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: { + closeOperation: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }, + }, + }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + await operation.close(); + + expect(driver.closeOperation.called).to.be.false; + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.true; + expect(driver.closeOperation.callCount).to.be.equal(0); + }); + + it('should throw an error in case of a status error and keep state', async () => { + const context = new ClientContextStub(); + context.driver.closeOperationResp.status.statusCode = TStatusCode.ERROR_STATUS; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + + try { + await operation.close(); + expect.fail('It should throw a StatusError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(StatusError); + expect(operation['cancelled']).to.be.false; + expect(operation['closed']).to.be.false; + } + }); + + it('should reject all methods once closed', async () => { + const context = new ClientContextStub(); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + await operation.close(); + expect(operation['closed']).to.be.true; + + await expectFailure(() => operation.fetchAll()); + await expectFailure(() => operation.fetchChunk({ disableBuffering: true })); + await expectFailure(() => operation.status()); + await expectFailure(() => operation.finished()); + await expectFailure(() => operation.getSchema()); + }); + }); + + describe('finished', () => { + [TOperationState.INITIALIZED_STATE, TOperationState.RUNNING_STATE, TOperationState.PENDING_STATE].forEach( + (operationState) => { + it(`should wait for finished state starting from TOperationState.${TOperationState[operationState]}`, async () => { + const attemptsUntilFinished = 3; + + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = operationState; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onCall(attemptsUntilFinished - 1) // count is zero-based + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(operation['state']).to.equal(TOperationState.INITIALIZED_STATE); + + await operation.finished(); + + expect(getOperationStatusStub.callCount).to.be.equal(attemptsUntilFinished); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + }); + }, + ); + + it('should request progress', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onSecondCall() + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + await operation.finished({ progress: true }); + + expect(getOperationStatusStub.called).to.be.true; + const request = getOperationStatusStub.getCall(0).args[0]; + expect(request.getProgressUpdate).to.be.true; + }); + + it('should invoke progress callback', async () => { + const attemptsUntilFinished = 3; + + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onCall(attemptsUntilFinished - 1) // count is zero-based + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const callback = sinon.stub(); + + await operation.finished({ callback }); + + expect(getOperationStatusStub.called).to.be.true; + expect(callback.callCount).to.be.equal(attemptsUntilFinished); + }); + + it('should pick up finished state from directResults', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.status.statusCode = TStatusCode.SUCCESS_STATUS; + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: { + operationStatus: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + hasResultSet: true, + }, + }, + }); + + await operation.finished(); + + // Once operation is finished - no need to fetch status again + expect(driver.getOperationStatus.called).to.be.false; + }); + + it('should throw an error in case of a status error', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.status.statusCode = TStatusCode.ERROR_STATUS; + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + try { + await operation.finished(); + expect.fail('It should throw a StatusError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(StatusError); + } + }); + + [ + TOperationState.CANCELED_STATE, + TOperationState.CLOSED_STATE, + TOperationState.ERROR_STATE, + TOperationState.UKNOWN_STATE, + TOperationState.TIMEDOUT_STATE, + ].forEach((operationState) => { + it(`should throw an error in case of a TOperationState.${TOperationState[operationState]}`, async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.status.statusCode = TStatusCode.SUCCESS_STATUS; + context.driver.getOperationStatusResp.operationState = operationState; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + try { + await operation.finished(); + expect.fail('It should throw a OperationStateError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(OperationStateError); + } + }); + }); + }); + + describe('getSchema', () => { + it('should return immediately if operation has no results', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = false; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: false }), context }); + + const schema = await operation.getSchema(); + + expect(schema).to.be.null; + expect(driver.getResultSetMetadata.called).to.be.false; + }); + + it('should wait for operation to complete', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onSecondCall() + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + context.driver.getResultSetMetadataResp.schema = { columns: [] }; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const schema = await operation.getSchema(); + + expect(getOperationStatusStub.called).to.be.true; + expect(schema).to.deep.equal(context.driver.getResultSetMetadataResp.schema); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + }); + + it('should request progress', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onSecondCall() + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + await operation.getSchema({ progress: true }); + + expect(getOperationStatusStub.called).to.be.true; + const request = getOperationStatusStub.getCall(0).args[0]; + expect(request.getProgressUpdate).to.be.true; + }); + + it('should invoke progress callback', async () => { + const attemptsUntilFinished = 3; + + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onCall(attemptsUntilFinished - 1) // count is zero-based + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const callback = sinon.stub(); + + await operation.getSchema({ callback }); + + expect(getOperationStatusStub.called).to.be.true; + expect(callback.callCount).to.be.equal(attemptsUntilFinished); + }); + + it('should fetch schema if operation has data', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const schema = await operation.getSchema(); + expect(schema).to.deep.equal(driver.getResultSetMetadataResp.schema); + expect(driver.getResultSetMetadata.called).to.be.true; + }); + + it('should return cached schema on subsequent calls', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const schema1 = await operation.getSchema(); + expect(schema1).to.deep.equal(context.driver.getResultSetMetadataResp.schema); + expect(driver.getResultSetMetadata.callCount).to.equal(1); + + const schema2 = await operation.getSchema(); + expect(schema2).to.deep.equal(context.driver.getResultSetMetadataResp.schema); + expect(driver.getResultSetMetadata.callCount).to.equal(1); // no additional requests + }); + + it('should use schema from directResults', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const directResults: TSparkDirectResults = { + resultSetMetadata: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + schema: { + columns: [ + { + columnName: 'another', + position: 0, + typeDesc: { + types: [ + { + primitiveEntry: { type: TTypeId.STRING_TYPE }, + }, + ], + }, + }, + ], + }, + }, + }; + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults, + }); + + const schema = await operation.getSchema(); + + expect(schema).to.deep.equal(directResults.resultSetMetadata?.schema); + expect(driver.getResultSetMetadata.called).to.be.false; + }); + + it('should throw an error in case of a status error', async () => { + const context = new ClientContextStub(); + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.getResultSetMetadataResp.status.statusCode = TStatusCode.ERROR_STATUS; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + try { + await operation.getSchema(); + expect.fail('It should throw a StatusError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(StatusError); + } + }); + + it('should use appropriate result handler', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + jsonHandler: { + driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.COLUMN_BASED_SET; + driver.getResultSetMetadata.resetHistory(); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + const resultHandler = await operation['getResultHandler'](); + expect(driver.getResultSetMetadata.called).to.be.true; + expect(resultHandler).to.be.instanceOf(ResultSlicer); + expect(resultHandler['source']).to.be.instanceOf(JsonResultHandler); + } + + arrowHandler: { + driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.ARROW_BASED_SET; + driver.getResultSetMetadata.resetHistory(); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + const resultHandler = await operation['getResultHandler'](); + expect(driver.getResultSetMetadata.called).to.be.true; + expect(resultHandler).to.be.instanceOf(ResultSlicer); + expect(resultHandler['source']).to.be.instanceOf(ArrowResultConverter); + if (!(resultHandler['source'] instanceof ArrowResultConverter)) { + throw new Error('Expected `resultHandler.source` to be `ArrowResultConverter`'); + } + expect(resultHandler['source']['source']).to.be.instanceOf(ArrowResultHandler); + } + + cloudFetchHandler: { + driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.URL_BASED_SET; + driver.getResultSetMetadata.resetHistory(); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + const resultHandler = await operation['getResultHandler'](); + expect(driver.getResultSetMetadata.called).to.be.true; + expect(resultHandler).to.be.instanceOf(ResultSlicer); + expect(resultHandler['source']).to.be.instanceOf(ArrowResultConverter); + if (!(resultHandler['source'] instanceof ArrowResultConverter)) { + throw new Error('Expected `resultHandler.source` to be `ArrowResultConverter`'); + } + expect(resultHandler['source']['source']).to.be.instanceOf(CloudFetchResultHandler); + } + }); + }); + + describe('fetchChunk', () => { + it('should return immediately if operation has no results', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: false }), context }); + + const results = await operation.fetchChunk({ disableBuffering: true }); + + expect(results).to.deep.equal([]); + expect(driver.getResultSetMetadata.called).to.be.false; + expect(driver.fetchResults.called).to.be.false; + }); + + it('should wait for operation to complete', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onSecondCall() + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + context.driver.getResultSetMetadataResp.schema = { columns: [] }; + context.driver.fetchResultsResp.hasMoreRows = false; + context.driver.fetchResultsResp.results!.columns = []; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const results = await operation.fetchChunk({ disableBuffering: true }); + + expect(getOperationStatusStub.called).to.be.true; + expect(results).to.deep.equal([]); + expect(operation['state']).to.equal(TOperationState.FINISHED_STATE); + }); + + it('should request progress', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onSecondCall() + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + context.driver.getResultSetMetadataResp.schema = { columns: [] }; + context.driver.fetchResultsResp.hasMoreRows = false; + context.driver.fetchResultsResp.results!.columns = []; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + await operation.fetchChunk({ progress: true, disableBuffering: true }); + + expect(getOperationStatusStub.called).to.be.true; + const request = getOperationStatusStub.getCall(0).args[0]; + expect(request.getProgressUpdate).to.be.true; + }); + + it('should invoke progress callback', async () => { + const attemptsUntilFinished = 3; + + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.INITIALIZED_STATE; + const getOperationStatusStub = sinon.stub(context.driver, 'getOperationStatus'); + + getOperationStatusStub + .callThrough() + .onCall(attemptsUntilFinished - 1) // count is zero-based + .callsFake((...args) => { + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + return getOperationStatusStub.wrappedMethod.apply(context.driver, args); + }); + + context.driver.getResultSetMetadataResp.schema = { columns: [] }; + context.driver.fetchResultsResp.hasMoreRows = false; + context.driver.fetchResultsResp.results!.columns = []; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const callback = sinon.stub(); + + await operation.fetchChunk({ callback, disableBuffering: true }); + + expect(getOperationStatusStub.called).to.be.true; + expect(callback.callCount).to.be.equal(attemptsUntilFinished); + }); + + it('should fetch schema and data and return array of records', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const results = await operation.fetchChunk({ disableBuffering: true }); + + expect(results).to.deep.equal([{ test: 'a' }, { test: 'b' }, { test: 'c' }]); + expect(driver.getResultSetMetadata.called).to.be.true; + expect(driver.fetchResults.called).to.be.true; + }); + + it('should return data from directResults (all the data in directResults)', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: { + resultSet: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + hasMoreRows: false, + results: { + startRowOffset: new Int64(0), + rows: [], + columns: [ + { + stringVal: { + values: ['a', 'b'], + nulls: Buffer.from([]), + }, + }, + ], + }, + }, + }, + }); + + const results = await operation.fetchChunk({ disableBuffering: true }); + + expect(results).to.deep.equal([{ test: 'a' }, { test: 'b' }]); + expect(driver.getResultSetMetadata.called).to.be.true; + expect(driver.fetchResults.called).to.be.false; + }); + + it('should return data from directResults (first chunk in directResults, next chunk fetched)', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + driver.getOperationStatusResp.hasResultSet = true; + + const operation = new DBSQLOperation({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: { + resultSet: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + hasMoreRows: true, + results: { + startRowOffset: new Int64(0), + rows: [], + columns: [ + { + stringVal: { + values: ['q', 'w'], + nulls: Buffer.from([]), + }, + }, + ], + }, + }, + }, + }); + + const results1 = await operation.fetchChunk({ disableBuffering: true }); + + expect(results1).to.deep.equal([{ test: 'q' }, { test: 'w' }]); + expect(driver.getResultSetMetadata.callCount).to.be.eq(1); + expect(driver.fetchResults.callCount).to.be.eq(0); + + const results2 = await operation.fetchChunk({ disableBuffering: true }); + + expect(results2).to.deep.equal([{ test: 'a' }, { test: 'b' }, { test: 'c' }]); + expect(driver.getResultSetMetadata.callCount).to.be.eq(1); + expect(driver.fetchResults.callCount).to.be.eq(1); + }); + + it('should fail on unsupported result format', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getResultSetMetadataResp.resultFormat = TSparkRowSetType.ROW_BASED_SET; + context.driver.getResultSetMetadataResp.schema = { columns: [] }; + + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + try { + await operation.fetchChunk({ disableBuffering: true }); + expect.fail('It should throw a HiveDriverError'); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.instanceOf(HiveDriverError); + } + }); + }); + + describe('fetchAll', () => { + it('should fetch data while available and return it all', async () => { + const context = new ClientContextStub(); + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + const originalData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; + + const tempData = [...originalData]; + const fetchChunkStub = sinon.stub(operation, 'fetchChunk').callsFake(async (): Promise> => { + return tempData.splice(0, 3); + }); + const hasMoreRowsStub = sinon.stub(operation, 'hasMoreRows').callsFake(async () => { + return tempData.length > 0; + }); + + const fetchedData = await operation.fetchAll(); + + // Warning: this check is implementation-specific + // `fetchAll` should wait for operation to complete. In current implementation + // it does so by calling `fetchChunk` at least once, which internally does + // all the job. But since here we stub `fetchChunk` it won't really wait, + // therefore here we ensure it was called at least once + expect(fetchChunkStub.callCount).to.be.gte(1); + + expect(fetchChunkStub.called).to.be.true; + expect(hasMoreRowsStub.called).to.be.true; + expect(fetchedData).to.deep.equal(originalData); + }); + }); + + describe('hasMoreRows', () => { + it('should return initial value prior to first fetch', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = false; + context.driver.fetchResultsResp.results = undefined; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.undefined; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.false; + expect(operation['_data']['hasMoreRowsFlag']).to.be.false; + }); + + it('should return False if operation was closed', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = true; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.true; + await operation.close(); + expect(await operation.hasMoreRows()).to.be.false; + }); + + it('should return False if operation was cancelled', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = true; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.true; + await operation.cancel(); + expect(await operation.hasMoreRows()).to.be.false; + }); + + it('should return True if hasMoreRows flag was set in response', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = true; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.undefined; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.true; + }); + + it('should return True if hasMoreRows flag is False but there is actual data', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = false; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.undefined; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.true; + }); + + it('should return True if hasMoreRows flag is unset but there is actual data', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = undefined; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.undefined; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.true; + }); + + it('should return False if hasMoreRows flag is False and there is no data', async () => { + const context = new ClientContextStub(); + + context.driver.getOperationStatusResp.operationState = TOperationState.FINISHED_STATE; + context.driver.getOperationStatusResp.hasResultSet = true; + context.driver.fetchResultsResp.hasMoreRows = false; + context.driver.fetchResultsResp.results = undefined; + const operation = new DBSQLOperation({ handle: operationHandleStub({ hasResultSet: true }), context }); + + expect(await operation.hasMoreRows()).to.be.true; + expect(operation['_data']['hasMoreRowsFlag']).to.be.undefined; + await operation.fetchChunk({ disableBuffering: true }); + expect(await operation.hasMoreRows()).to.be.false; + expect(operation['_data']['hasMoreRowsFlag']).to.be.false; + }); + }); +}); diff --git a/tests/unit/DBSQLParameter.test.js b/tests/unit/DBSQLParameter.test.ts similarity index 86% rename from tests/unit/DBSQLParameter.test.js rename to tests/unit/DBSQLParameter.test.ts index 8f92ae29..a3f7659e 100644 --- a/tests/unit/DBSQLParameter.test.js +++ b/tests/unit/DBSQLParameter.test.ts @@ -1,12 +1,11 @@ -const { expect } = require('chai'); - -const Int64 = require('node-int64'); -const { TSparkParameterValue, TSparkParameter } = require('../../thrift/TCLIService_types'); -const { DBSQLParameter, DBSQLParameterType } = require('../../lib/DBSQLParameter'); +import { expect } from 'chai'; +import Int64 from 'node-int64'; +import { TSparkParameterValue, TSparkParameter } from '../../thrift/TCLIService_types'; +import { DBSQLParameter, DBSQLParameterType, DBSQLParameterValue } from '../../lib/DBSQLParameter'; describe('DBSQLParameter', () => { it('should infer types correctly', () => { - const cases = [ + const cases: Array<[DBSQLParameterValue, TSparkParameter]> = [ [ false, new TSparkParameter({ @@ -72,9 +71,9 @@ describe('DBSQLParameter', () => { }); it('should use provided type', () => { - const expectedType = '_CUSTOM_TYPE_'; // it doesn't have to be valid type name, just any string + const expectedType = '_CUSTOM_TYPE_' as DBSQLParameterType; // it doesn't have to be valid type name, just any string - const cases = [ + const cases: Array<[DBSQLParameterValue, TSparkParameter]> = [ [false, new TSparkParameter({ type: expectedType, value: new TSparkParameterValue({ stringValue: 'FALSE' }) })], [true, new TSparkParameter({ type: expectedType, value: new TSparkParameterValue({ stringValue: 'TRUE' }) })], [123, new TSparkParameter({ type: expectedType, value: new TSparkParameterValue({ stringValue: '123' }) })], diff --git a/tests/unit/DBSQLSession.test.js b/tests/unit/DBSQLSession.test.ts similarity index 61% rename from tests/unit/DBSQLSession.test.js rename to tests/unit/DBSQLSession.test.ts index bfa5b4bc..460047f5 100644 --- a/tests/unit/DBSQLSession.test.js +++ b/tests/unit/DBSQLSession.test.ts @@ -1,57 +1,18 @@ -const { expect, AssertionError } = require('chai'); -const { DBSQLLogger, LogLevel } = require('../../lib'); -const sinon = require('sinon'); -const Int64 = require('node-int64'); -const { default: DBSQLSession, numberToInt64 } = require('../../lib/DBSQLSession'); -const InfoValue = require('../../lib/dto/InfoValue').default; -const Status = require('../../lib/dto/Status').default; -const DBSQLOperation = require('../../lib/DBSQLOperation').default; -const HiveDriver = require('../../lib/hive/HiveDriver').default; -const DBSQLClient = require('../../lib/DBSQLClient').default; - -// Create logger that won't emit -// -const logger = new DBSQLLogger({ level: LogLevel.error }); - -function createDriverMock(customMethodHandler) { - customMethodHandler = customMethodHandler || ((methodName, value) => value); - - const driver = new HiveDriver({}); - - return new Proxy(driver, { - get: function (target, prop) { - // Mock only methods of driver - if (typeof target[prop] === 'function') { - return () => - Promise.resolve( - customMethodHandler(prop, { - status: { - statusCode: 0, - }, - operationHandle: 'operationHandle', - infoValue: {}, - }), - ); - } - }, - }); -} - -function createSession(customMethodHandler) { - const driver = createDriverMock(customMethodHandler); - const clientConfig = DBSQLClient.getDefaultConfig(); - - return new DBSQLSession({ - handle: { sessionId: 'id' }, - context: { - getConfig: () => clientConfig, - getLogger: () => logger, - getDriver: async () => driver, - }, - }); -} - -async function expectFailure(fn) { +import { AssertionError, expect } from 'chai'; +import sinon, { SinonSpy } from 'sinon'; +import Int64 from 'node-int64'; +import DBSQLSession, { numberToInt64 } from '../../lib/DBSQLSession'; +import InfoValue from '../../lib/dto/InfoValue'; +import Status from '../../lib/dto/Status'; +import DBSQLOperation from '../../lib/DBSQLOperation'; +import { TSessionHandle } from '../../thrift/TCLIService_types'; +import ClientContextStub from './.stubs/ClientContextStub'; + +const sessionHandleStub: TSessionHandle = { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, +}; + +async function expectFailure(fn: () => Promise) { try { await fn(); expect.fail('It should throw an error'); @@ -89,7 +50,7 @@ describe('DBSQLSession', () => { describe('getInfo', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getInfo(1); expect(result).instanceOf(InfoValue); }); @@ -97,73 +58,49 @@ describe('DBSQLSession', () => { describe('executeStatement', () => { it('should execute statement', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.executeStatement('SELECT * FROM table'); expect(result).instanceOf(DBSQLOperation); }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.executeStatement('SELECT * FROM table', { maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.executeStatement('SELECT * FROM table', { maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); describe('Arrow support', () => { it('should not use Arrow if disabled in options', async () => { - const session = createSession(); - const result = await session.executeStatement('SELECT * FROM table', { enableArrow: false }); + const session = new DBSQLSession({ + handle: sessionHandleStub, + context: new ClientContextStub({ arrowEnabled: false }), + }); + const result = await session.executeStatement('SELECT * FROM table'); expect(result).instanceOf(DBSQLOperation); }); it('should apply defaults for Arrow options', async () => { - const session = createSession(); - case1: { - const result = await session.executeStatement('SELECT * FROM table', { enableArrow: true }); - expect(result).instanceOf(DBSQLOperation); - } - - case2: { - const result = await session.executeStatement('SELECT * FROM table', { - enableArrow: true, - arrowOptions: {}, + const session = new DBSQLSession({ + handle: sessionHandleStub, + context: new ClientContextStub({ arrowEnabled: true }), }); + const result = await session.executeStatement('SELECT * FROM table'); expect(result).instanceOf(DBSQLOperation); } - case3: { - const result = await session.executeStatement('SELECT * FROM table', { - enableArrow: true, - arrowOptions: { - useNativeTimestamps: false, - }, - }); - expect(result).instanceOf(DBSQLOperation); - } - - case4: { - const result = await session.executeStatement('SELECT * FROM table', { - enableArrow: true, - arrowOptions: { - useNativeDecimals: false, - }, - }); - expect(result).instanceOf(DBSQLOperation); - } - - case5: { - const result = await session.executeStatement('SELECT * FROM table', { - enableArrow: true, - arrowOptions: { - useNativeComplexTypes: false, - }, + case2: { + const session = new DBSQLSession({ + handle: sessionHandleStub, + context: new ClientContextStub({ arrowEnabled: true, useArrowNativeTypes: false }), }); + const result = await session.executeStatement('SELECT * FROM table'); expect(result).instanceOf(DBSQLOperation); } }); @@ -172,19 +109,19 @@ describe('DBSQLSession', () => { describe('getTypeInfo', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTypeInfo(); expect(result).instanceOf(DBSQLOperation); }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTypeInfo({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTypeInfo({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -192,19 +129,19 @@ describe('DBSQLSession', () => { describe('getCatalogs', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCatalogs(); expect(result).instanceOf(DBSQLOperation); }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCatalogs({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCatalogs({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -212,13 +149,13 @@ describe('DBSQLSession', () => { describe('getSchemas', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getSchemas(); expect(result).instanceOf(DBSQLOperation); }); it('should use filters', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getSchemas({ catalogName: 'catalog', schemaName: 'schema', @@ -227,13 +164,13 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getSchemas({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getSchemas({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -241,13 +178,13 @@ describe('DBSQLSession', () => { describe('getTables', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTables(); expect(result).instanceOf(DBSQLOperation); }); it('should use filters', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTables({ catalogName: 'catalog', schemaName: 'default', @@ -258,13 +195,13 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTables({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTables({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -272,19 +209,19 @@ describe('DBSQLSession', () => { describe('getTableTypes', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTableTypes(); expect(result).instanceOf(DBSQLOperation); }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTableTypes({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getTableTypes({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -292,13 +229,13 @@ describe('DBSQLSession', () => { describe('getColumns', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getColumns(); expect(result).instanceOf(DBSQLOperation); }); it('should use filters', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getColumns({ catalogName: 'catalog', schemaName: 'schema', @@ -309,13 +246,13 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getColumns({ maxRows: 10 }); expect(result).instanceOf(DBSQLOperation); }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getColumns({ maxRows: null }); expect(result).instanceOf(DBSQLOperation); }); @@ -323,7 +260,7 @@ describe('DBSQLSession', () => { describe('getFunctions', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getFunctions({ catalogName: 'catalog', schemaName: 'schema', @@ -333,7 +270,7 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getFunctions({ catalogName: 'catalog', schemaName: 'schema', @@ -344,7 +281,7 @@ describe('DBSQLSession', () => { }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getFunctions({ catalogName: 'catalog', schemaName: 'schema', @@ -357,7 +294,7 @@ describe('DBSQLSession', () => { describe('getPrimaryKeys', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getPrimaryKeys({ catalogName: 'catalog', schemaName: 'schema', @@ -367,7 +304,7 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getPrimaryKeys({ catalogName: 'catalog', schemaName: 'schema', @@ -378,7 +315,7 @@ describe('DBSQLSession', () => { }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getPrimaryKeys({ catalogName: 'catalog', schemaName: 'schema', @@ -391,7 +328,7 @@ describe('DBSQLSession', () => { describe('getCrossReference', () => { it('should run operation', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCrossReference({ parentCatalogName: 'parentCatalogName', parentSchemaName: 'parentSchemaName', @@ -404,7 +341,7 @@ describe('DBSQLSession', () => { }); it('should use direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCrossReference({ parentCatalogName: 'parentCatalogName', parentSchemaName: 'parentSchemaName', @@ -418,7 +355,7 @@ describe('DBSQLSession', () => { }); it('should disable direct results', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const result = await session.getCrossReference({ parentCatalogName: 'parentCatalogName', parentSchemaName: 'parentSchemaName', @@ -434,64 +371,62 @@ describe('DBSQLSession', () => { describe('close', () => { it('should run operation', async () => { - const driverMethodStub = sinon.stub().returns({ - status: { - statusCode: 0, - }, - }); + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); - const session = createSession(driverMethodStub); - expect(session.isOpen).to.be.true; + const session = new DBSQLSession({ handle: sessionHandleStub, context }); + expect(session['isOpen']).to.be.true; const result = await session.close(); expect(result).instanceOf(Status); - expect(session.isOpen).to.be.false; - expect(driverMethodStub.callCount).to.eq(1); + expect(session['isOpen']).to.be.false; + expect(driver.closeSession.callCount).to.eq(1); }); it('should not run operation twice', async () => { - const driverMethodStub = sinon.stub().returns({ - status: { - statusCode: 0, - }, - }); + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); - const session = createSession(driverMethodStub); - expect(session.isOpen).to.be.true; + const session = new DBSQLSession({ handle: sessionHandleStub, context }); + expect(session['isOpen']).to.be.true; const result = await session.close(); expect(result).instanceOf(Status); - expect(session.isOpen).to.be.false; - expect(driverMethodStub.callCount).to.eq(1); + expect(session['isOpen']).to.be.false; + expect(driver.closeSession.callCount).to.eq(1); const result2 = await session.close(); expect(result2).instanceOf(Status); - expect(session.isOpen).to.be.false; - expect(driverMethodStub.callCount).to.eq(1); // second time it should not be called + expect(session['isOpen']).to.be.false; + expect(driver.closeSession.callCount).to.eq(1); // second time it should not be called }); it('should close operations that belong to it', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); const operation = await session.executeStatement('SELECT * FROM table'); + if (!(operation instanceof DBSQLOperation)) { + expect.fail('Assertion error: operation is not a DBSQLOperation'); + } + expect(operation.onClose).to.be.not.undefined; - expect(operation.closed).to.be.false; - expect(session.operations.items.size).to.eq(1); + expect(operation['closed']).to.be.false; + expect(session['operations']['items'].size).to.eq(1); - sinon.spy(session.operations, 'closeAll'); + sinon.spy(session['operations'], 'closeAll'); sinon.spy(operation, 'close'); await session.close(); - expect(operation.close.called).to.be.true; - expect(session.operations.closeAll.called).to.be.true; + expect((operation.close as SinonSpy).called).to.be.true; + expect((session['operations'].closeAll as SinonSpy).called).to.be.true; expect(operation.onClose).to.be.undefined; - expect(operation.closed).to.be.true; - expect(session.operations.items.size).to.eq(0); + expect(operation['closed']).to.be.true; + expect(session['operations']['items'].size).to.eq(0); }); it('should reject all methods once closed', async () => { - const session = createSession(); + const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() }); await session.close(); - expect(session.isOpen).to.be.false; + expect(session['isOpen']).to.be.false; await expectFailure(() => session.getInfo(1)); await expectFailure(() => session.executeStatement('SELECT * FROM table')); @@ -501,9 +436,27 @@ describe('DBSQLSession', () => { await expectFailure(() => session.getTables()); await expectFailure(() => session.getTableTypes()); await expectFailure(() => session.getColumns()); - await expectFailure(() => session.getFunctions()); - await expectFailure(() => session.getPrimaryKeys()); - await expectFailure(() => session.getCrossReference()); + await expectFailure(() => + session.getFunctions({ + functionName: 'func', + }), + ); + await expectFailure(() => + session.getPrimaryKeys({ + schemaName: 'schema', + tableName: 'table', + }), + ); + await expectFailure(() => + session.getCrossReference({ + parentCatalogName: 'parent_catalog', + parentSchemaName: 'parent_schema', + parentTableName: 'parent_table', + foreignCatalogName: 'foreign_catalog', + foreignSchemaName: 'foreign_schema', + foreignTableName: 'foreign_table', + }), + ); }); }); }); diff --git a/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.js b/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.js deleted file mode 100644 index 55338840..00000000 --- a/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.js +++ /dev/null @@ -1,282 +0,0 @@ -const { expect, AssertionError } = require('chai'); -const { EventEmitter } = require('events'); -const sinon = require('sinon'); -const http = require('http'); -const { DBSQLLogger, LogLevel } = require('../../../../../lib'); -const AuthorizationCode = require('../../../../../lib/connection/auth/DatabricksOAuth/AuthorizationCode').default; - -const logger = new DBSQLLogger({ level: LogLevel.error }); - -class HttpServerMock extends EventEmitter { - constructor() { - super(); - this.requestHandler = () => {}; - this.listening = false; - this.listenError = undefined; // error to emit on listen - this.closeError = undefined; // error to emit on close - } - - listen(port, host, callback) { - if (this.listenError) { - this.emit('error', this.listenError); - this.listenError = undefined; - } else if (port < 1000) { - const error = new Error(`Address ${host}:${port} is already in use`); - error.code = 'EADDRINUSE'; - this.emit('error', error); - } else { - this.listening = true; - callback(); - } - } - - close(callback) { - this.requestHandler = () => {}; - this.listening = false; - if (this.closeError) { - this.emit('error', this.closeError); - this.closeError = undefined; - } else { - callback(); - } - } -} - -class OAuthClientMock { - constructor() { - this.code = 'test_authorization_code'; - this.redirectUri = undefined; - } - - authorizationUrl(params) { - this.redirectUri = params.redirect_uri; - return JSON.stringify({ - state: params.state, - code: this.code, - }); - } - - callbackParams(req) { - return req.params; - } -} - -function prepareTestInstances(options) { - const httpServer = new HttpServerMock(); - - const oauthClient = new OAuthClientMock(); - - const authCode = new AuthorizationCode({ - client: oauthClient, - ...options, - context: { - getLogger: () => logger, - }, - }); - - sinon.stub(http, 'createServer').callsFake((requestHandler) => { - httpServer.requestHandler = requestHandler; - return httpServer; - }); - - sinon.stub(authCode, 'openUrl').callsFake((url) => { - const params = JSON.parse(url); - httpServer.requestHandler( - { params }, - { - writeHead: () => {}, - end: () => {}, - }, - ); - }); - - function reloadUrl() { - setTimeout(() => { - const args = authCode.openUrl.firstCall.args; - authCode.openUrl(...args); - }, 10); - } - - return { httpServer, oauthClient, authCode, reloadUrl }; -} - -describe('AuthorizationCode', () => { - afterEach(() => { - http.createServer.restore?.(); - }); - - it('should fetch authorization code', async () => { - const { authCode, oauthClient } = prepareTestInstances({ - ports: [80, 8000], - logger: { log: () => {} }, - }); - - const result = await authCode.fetch([]); - expect(http.createServer.callCount).to.be.equal(2); - expect(authCode.openUrl.callCount).to.be.equal(1); - - expect(result.code).to.be.equal(oauthClient.code); - expect(result.verifier).to.not.be.empty; - expect(result.redirectUri).to.be.equal(oauthClient.redirectUri); - }); - - it('should throw error if cannot start server on any port', async () => { - const { authCode } = prepareTestInstances({ - ports: [80, 443], - }); - - try { - await authCode.fetch([]); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(http.createServer.callCount).to.be.equal(2); - expect(authCode.openUrl.callCount).to.be.equal(0); - - expect(error.message).to.contain('all ports are in use'); - } - }); - - it('should re-throw unhandled server start errors', async () => { - const { authCode, httpServer } = prepareTestInstances({ - ports: [80], - }); - - const testError = new Error('Test'); - httpServer.listenError = testError; - - try { - await authCode.fetch([]); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(0); - - expect(error).to.be.equal(testError); - } - }); - - it('should re-throw unhandled server stop errors', async () => { - const { authCode, httpServer } = prepareTestInstances({ - ports: [8000], - }); - - const testError = new Error('Test'); - httpServer.closeError = testError; - - try { - await authCode.fetch([]); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(1); - - expect(error).to.be.equal(testError); - } - }); - - it('should throw an error if no code was returned', async () => { - const { authCode, oauthClient } = prepareTestInstances({ - ports: [8000], - }); - - sinon.stub(oauthClient, 'callbackParams').callsFake((req) => { - // Omit authorization code from params - const { code, ...otherParams } = req.params; - return otherParams; - }); - - try { - await authCode.fetch([]); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(1); - - expect(error.message).to.contain('No path parameters were returned to the callback'); - } - }); - - it('should use error details from callback params', async () => { - const { authCode, oauthClient } = prepareTestInstances({ - ports: [8000], - }); - - sinon.stub(oauthClient, 'callbackParams').callsFake((req) => { - // Omit authorization code from params - const { code, ...otherParams } = req.params; - return { - ...otherParams, - error: 'test_error', - error_description: 'Test error', - }; - }); - - try { - await authCode.fetch([]); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(1); - - expect(error.message).to.contain('Test error'); - } - }); - - it('should serve 404 for unrecognized requests', async () => { - const { authCode, oauthClient, reloadUrl } = prepareTestInstances({ - ports: [8000], - }); - - sinon - .stub(oauthClient, 'callbackParams') - .onFirstCall() - .callsFake(() => { - // Repeat the same request after currently processed one. - // We won't modify response on subsequent requests so OAuth routine can complete - reloadUrl(); - // Return no params so request cannot be recognized - return {}; - }) - .callThrough(); - - await authCode.fetch([]); - - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(2); - }); - - it('should not attempt to stop server if not running', async () => { - const { authCode, oauthClient, httpServer } = prepareTestInstances({ - ports: [8000], - logger: { log: () => {} }, - }); - - const promise = authCode.fetch([]); - - httpServer.listening = false; - httpServer.closeError = new Error('Test'); - - const result = await promise; - // We set up server to throw an error on close. If nothing happened - it means - // that `authCode` never tried to stop it - expect(result.code).to.be.equal(oauthClient.code); - - expect(http.createServer.callCount).to.be.equal(1); - expect(authCode.openUrl.callCount).to.be.equal(1); - }); -}); diff --git a/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.ts b/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.ts new file mode 100644 index 00000000..9b109f1c --- /dev/null +++ b/tests/unit/connection/auth/DatabricksOAuth/AuthorizationCode.test.ts @@ -0,0 +1,293 @@ +import { expect, AssertionError } from 'chai'; +import sinon from 'sinon'; +import net from 'net'; +import { IncomingMessage, ServerResponse } from 'http'; +import { BaseClient, AuthorizationParameters, CallbackParamsType, custom } from 'openid-client'; +import AuthorizationCode, { + AuthorizationCodeOptions, +} from '../../../../../lib/connection/auth/DatabricksOAuth/AuthorizationCode'; + +import ClientContextStub from '../../../.stubs/ClientContextStub'; +import { OAuthCallbackServerStub } from '../../../.stubs/OAuth'; + +class IncomingMessageStub extends IncomingMessage { + public params: CallbackParamsType; + + constructor(params: CallbackParamsType = {}) { + super(new net.Socket()); + this.params = params; + } +} + +class ServerResponseStub extends ServerResponse {} + +// `BaseClient` is not actually exported from `openid-client`, just declared. So instead of extending it, +// we use it as an interface and declare all the dummy properties we're not going to use anyway +class OpenIDClientStub implements BaseClient { + public code = 'test_authorization_code'; + + public redirectUri?: string = undefined; + + authorizationUrl(params: AuthorizationParameters) { + this.redirectUri = params.redirect_uri; + return JSON.stringify({ + state: params.state, + code: this.code, + }); + } + + callbackParams(req: IncomingMessage) { + return req instanceof IncomingMessageStub ? req.params : {}; + } + + // All the unused properties from `BaseClient` + public metadata: any; + + public issuer: any; + + public endSessionUrl: any; + + public callback: any; + + public oauthCallback: any; + + public refresh: any; + + public userinfo: any; + + public requestResource: any; + + public grant: any; + + public introspect: any; + + public revoke: any; + + public requestObject: any; + + public deviceAuthorization: any; + + public pushedAuthorizationRequest: any; + + public [custom.http_options]: any; + + public [custom.clock_tolerance]: any; + + [key: string]: unknown; +} + +function prepareTestInstances(options: Partial) { + const oauthClient = new OpenIDClientStub(); + + const httpServer = new OAuthCallbackServerStub(); + + const openAuthUrl = sinon.stub<[string], Promise>(); + + const authCode = new AuthorizationCode({ + client: oauthClient, + context: new ClientContextStub(), + ports: [], + ...options, + openAuthUrl, + }); + + const authCodeSpy = sinon.spy(authCode); + + const createHttpServer = sinon.spy((requestHandler: (req: IncomingMessage, res: ServerResponse) => void) => { + httpServer.requestHandler = requestHandler; + return httpServer; + }); + + authCode['createHttpServer'] = createHttpServer; + + openAuthUrl.callsFake(async (authUrl) => { + const params = JSON.parse(authUrl); + const req = new IncomingMessageStub(params); + const resp = new ServerResponseStub(req); + httpServer.requestHandler(req, resp); + }); + + function reloadUrl() { + setTimeout(() => { + const args = openAuthUrl.firstCall.args; + openAuthUrl(...args); + }, 10); + } + + return { oauthClient, authCode: authCodeSpy, httpServer, openAuthUrl, reloadUrl, createHttpServer }; +} + +describe('AuthorizationCode', () => { + it('should fetch authorization code', async () => { + const { authCode, oauthClient, openAuthUrl, createHttpServer } = prepareTestInstances({ + ports: [80, 8000], + }); + + const result = await authCode.fetch([]); + expect(createHttpServer.callCount).to.be.equal(2); + expect(openAuthUrl.callCount).to.be.equal(1); + + expect(result.code).to.be.equal(oauthClient.code); + expect(result.verifier).to.not.be.empty; + expect(result.redirectUri).to.be.equal(oauthClient.redirectUri); + }); + + it('should throw error if cannot start server on any port', async () => { + const { authCode, openAuthUrl, createHttpServer } = prepareTestInstances({ + ports: [80, 443], + }); + + try { + await authCode.fetch([]); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(createHttpServer.callCount).to.be.equal(2); + expect(openAuthUrl.callCount).to.be.equal(0); + + expect(error.message).to.contain('all ports are in use'); + } + }); + + it('should re-throw unhandled server start errors', async () => { + const { authCode, openAuthUrl, httpServer, createHttpServer } = prepareTestInstances({ + ports: [80], + }); + + const testError = new Error('Test'); + httpServer.listenError = testError; + + try { + await authCode.fetch([]); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(0); + + expect(error).to.be.equal(testError); + } + }); + + it('should re-throw unhandled server stop errors', async () => { + const { authCode, openAuthUrl, httpServer, createHttpServer } = prepareTestInstances({ + ports: [8000], + }); + + const testError = new Error('Test'); + httpServer.closeError = testError; + + try { + await authCode.fetch([]); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(1); + + expect(error).to.be.equal(testError); + } + }); + + it('should throw an error if no code was returned', async () => { + const { authCode, oauthClient, openAuthUrl, createHttpServer } = prepareTestInstances({ + ports: [8000], + }); + + sinon.stub(oauthClient, 'callbackParams').callsFake((req) => { + // Omit authorization code from params + const { code, ...otherParams } = req instanceof IncomingMessageStub ? req.params : { code: undefined }; + return otherParams; + }); + + try { + await authCode.fetch([]); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(1); + + expect(error.message).to.contain('No path parameters were returned to the callback'); + } + }); + + it('should use error details from callback params', async () => { + const { authCode, oauthClient, openAuthUrl, createHttpServer } = prepareTestInstances({ + ports: [8000], + }); + + sinon.stub(oauthClient, 'callbackParams').callsFake((req) => { + // Omit authorization code from params + const { code, ...otherParams } = req instanceof IncomingMessageStub ? req.params : { code: undefined }; + return { + ...otherParams, + error: 'test_error', + error_description: 'Test error', + }; + }); + + try { + await authCode.fetch([]); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(1); + + expect(error.message).to.contain('Test error'); + } + }); + + it('should serve 404 for unrecognized requests', async () => { + const { authCode, oauthClient, reloadUrl, openAuthUrl, createHttpServer } = prepareTestInstances({ + ports: [8000], + }); + + sinon + .stub(oauthClient, 'callbackParams') + .onFirstCall() + .callsFake(() => { + // Repeat the same request after currently processed one. + // We won't modify response on subsequent requests so OAuth routine can complete + reloadUrl(); + // Return no params so request cannot be recognized + return {}; + }) + .callThrough(); + + await authCode.fetch([]); + + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(2); + }); + + it('should not attempt to stop server if not running', async () => { + const { authCode, oauthClient, openAuthUrl, httpServer, createHttpServer } = prepareTestInstances({ + ports: [8000], + }); + + const promise = authCode.fetch([]); + + httpServer.listening = false; + httpServer.closeError = new Error('Test'); + + const result = await promise; + // We set up server to throw an error on close. If nothing happened - it means + // that `authCode` never tried to stop it + expect(result.code).to.be.equal(oauthClient.code); + + expect(createHttpServer.callCount).to.be.equal(1); + expect(openAuthUrl.callCount).to.be.equal(1); + }); +}); diff --git a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.js b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts similarity index 73% rename from tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.js rename to tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts index 8bd2af0b..c2367971 100644 --- a/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.js +++ b/tests/unit/connection/auth/DatabricksOAuth/OAuthManager.test.ts @@ -1,52 +1,42 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const openidClientLib = require('openid-client'); -const { DBSQLLogger, LogLevel } = require('../../../../../lib'); -const { - DatabricksOAuthManager, +import { AssertionError, expect } from 'chai'; +import sinon, { SinonStub } from 'sinon'; +import { Issuer, BaseClient, TokenSet, GrantBody, IssuerMetadata, ClientMetadata, custom } from 'openid-client'; +// Import the whole module once more - to stub some of its exports +import openidClientLib from 'openid-client'; +// Import the whole module to stub its default export +import * as AuthorizationCodeModule from '../../../../../lib/connection/auth/DatabricksOAuth/AuthorizationCode'; + +import { AzureOAuthManager, + DatabricksOAuthManager, OAuthFlow, -} = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthManager'); -const OAuthToken = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken').default; -const { OAuthScope, scopeDelimiter } = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthScope'); -const AuthorizationCodeModule = require('../../../../../lib/connection/auth/DatabricksOAuth/AuthorizationCode'); + OAuthManagerOptions, +} from '../../../../../lib/connection/auth/DatabricksOAuth/OAuthManager'; +import OAuthToken from '../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken'; +import { OAuthScope, scopeDelimiter } from '../../../../../lib/connection/auth/DatabricksOAuth/OAuthScope'; +import { AuthorizationCodeStub, createExpiredAccessToken, createValidAccessToken } from '../../../.stubs/OAuth'; +import ClientContextStub from '../../../.stubs/ClientContextStub'; -const { createValidAccessToken, createExpiredAccessToken } = require('./utils'); +// `BaseClient` is not actually exported from `openid-client`, just declared. So instead of extending it, +// we use it as an interface and declare all the dummy properties we're not going to use anyway +class OpenIDClientStub implements BaseClient { + public clientOptions: ClientMetadata = { client_id: 'test_client' }; -const logger = new DBSQLLogger({ level: LogLevel.error }); + public expectedClientId?: string = undefined; -class AuthorizationCodeMock { - constructor() { - this.fetchResult = undefined; - this.expectedScope = undefined; - } + public expectedClientSecret?: string = undefined; - async fetch(scopes) { - if (this.expectedScope) { - expect(scopes.join(scopeDelimiter)).to.be.equal(this.expectedScope); - } - return this.fetchResult; - } -} + public expectedScope?: string = undefined; -AuthorizationCodeMock.validCode = { - code: 'auth_code', - verifier: 'verifier_string', - redirectUri: 'http://localhost:8000', -}; + public grantError?: Error = undefined; -class OAuthClientMock { - constructor() { - this.clientOptions = {}; - this.expectedClientId = undefined; - this.expectedClientSecret = undefined; - this.expectedScope = undefined; + public refreshError?: Error = undefined; + + public accessToken?: string = undefined; - this.grantError = undefined; - this.refreshError = undefined; + public refreshToken?: string = undefined; - this.accessToken = undefined; - this.refreshToken = undefined; + constructor() { this.recreateTokens(); } @@ -56,7 +46,7 @@ class OAuthClientMock { this.refreshToken = `refresh.${suffix}`; } - async grantU2M(params) { + async grantU2M(params: GrantBody) { if (this.grantError) { const error = this.grantError; this.grantError = undefined; @@ -64,20 +54,20 @@ class OAuthClientMock { } expect(params.grant_type).to.be.equal('authorization_code'); - expect(params.code).to.be.equal(AuthorizationCodeMock.validCode.code); - expect(params.code_verifier).to.be.equal(AuthorizationCodeMock.validCode.verifier); - expect(params.redirect_uri).to.be.equal(AuthorizationCodeMock.validCode.redirectUri); + expect(params.code).to.be.equal(AuthorizationCodeStub.validCode.code); + expect(params.code_verifier).to.be.equal(AuthorizationCodeStub.validCode.verifier); + expect(params.redirect_uri).to.be.equal(AuthorizationCodeStub.validCode.redirectUri); if (this.expectedScope) { expect(params.scope).to.be.equal(this.expectedScope); } - return { + return new TokenSet({ access_token: this.accessToken, refresh_token: this.refreshToken, - }; + }); } - async grantM2M(params) { + async grantM2M(params: GrantBody) { if (this.grantError) { const error = this.grantError; this.grantError = undefined; @@ -89,23 +79,23 @@ class OAuthClientMock { expect(params.scope).to.be.equal(this.expectedScope); } - return { + return new TokenSet({ access_token: this.accessToken, refresh_token: this.refreshToken, - }; + }); } - async grant(params) { + async grant(params: GrantBody) { switch (this.clientOptions.token_endpoint_auth_method) { case 'client_secret_basic': return this.grantM2M(params); case 'none': return this.grantU2M(params); } - throw new Error(`OAuthClientMock: unrecognized auth method: ${this.clientOptions.token_endpoint_auth_method}`); + throw new Error(`OAuthClientStub: unrecognized auth method: ${this.clientOptions.token_endpoint_auth_method}`); } - async refresh(refreshToken) { + async refresh(refreshToken: string) { if (this.refreshError) { const error = this.refreshError; this.refreshError = undefined; @@ -115,45 +105,95 @@ class OAuthClientMock { expect(refreshToken).to.be.equal(this.refreshToken); this.recreateTokens(); - return { + return new TokenSet({ access_token: this.accessToken, refresh_token: this.refreshToken, - }; + }); } + + // All the unused properties from `BaseClient` + public metadata: any; + + public issuer: any; + + public authorizationUrl: any; + + public callbackParams: any; + + public endSessionUrl: any; + + public callback: any; + + public oauthCallback: any; + + public userinfo: any; + + public requestResource: any; + + public introspect: any; + + public revoke: any; + + public requestObject: any; + + public deviceAuthorization: any; + + public pushedAuthorizationRequest: any; + + public [custom.http_options]: any; + + public [custom.clock_tolerance]: any; + + [key: string]: unknown; + + public static [custom.http_options]: any; + + public static [custom.clock_tolerance]: any; } [DatabricksOAuthManager, AzureOAuthManager].forEach((OAuthManagerClass) => { - function prepareTestInstances(options) { - const oauthClient = new OAuthClientMock(); - sinon.stub(oauthClient, 'grant').callThrough(); - sinon.stub(oauthClient, 'refresh').callThrough(); + afterEach(() => { + (openidClientLib.Issuer as unknown as SinonStub).restore?.(); + (AuthorizationCodeModule.default as unknown as SinonStub).restore?.(); + }); + + function prepareTestInstances(options: Partial) { + const oauthClient = sinon.spy(new OpenIDClientStub()); oauthClient.expectedClientId = options?.clientId; oauthClient.expectedClientSecret = options?.clientSecret; - const issuer = { - Client: function (clientOptions) { - oauthClient.clientOptions = clientOptions; - return oauthClient; + const issuer: Issuer = { + Client: class extends OpenIDClientStub { + constructor(clientOptions: ClientMetadata) { + super(); + oauthClient.clientOptions = clientOptions; + return oauthClient; + } }, + + FAPI1Client: OpenIDClientStub, + + metadata: { issuer: 'test' }, + [custom.http_options]: () => ({}), + + discover: async () => issuer, }; sinon.stub(openidClientLib, 'Issuer').returns(issuer); - openidClientLib.Issuer.discover = () => Promise.resolve(issuer); + // Now `openidClientLib.Issuer` is a Sinon wrapper function which doesn't have a `discover` method. + // It is safe to just assign it (`sinon.stub` won't work anyway) + openidClientLib.Issuer.discover = async () => issuer; const oauthManager = new OAuthManagerClass({ host: 'https://example.com', + flow: OAuthFlow.M2M, ...options, - context: { - getLogger: () => logger, - getConnectionProvider: async () => ({ - getAgent: async () => undefined, - }), - }, + context: new ClientContextStub(), }); - const authCode = new AuthorizationCodeMock(); - authCode.fetchResult = { ...AuthorizationCodeMock.validCode }; + const authCode = new AuthorizationCodeStub(); + authCode.fetchResult = { ...AuthorizationCodeStub.validCode }; sinon.stub(AuthorizationCodeModule, 'default').returns(authCode); @@ -161,13 +201,8 @@ class OAuthClientMock { } describe(OAuthManagerClass.name, () => { - afterEach(() => { - AuthorizationCodeModule.default.restore?.(); - openidClientLib.Issuer.restore?.(); - }); - describe('U2M flow', () => { - function getExpectedScope(scopes) { + function getExpectedScope(scopes: Array) { switch (OAuthManagerClass) { case DatabricksOAuthManager: return [...scopes].join(scopeDelimiter); @@ -203,7 +238,7 @@ class OAuthClientMock { await oauthManager.getToken(requestedScopes); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(oauthClient.grant.called).to.be.true; @@ -213,7 +248,7 @@ class OAuthClientMock { it('should re-throw unhandled errors when getting access token', async () => { const { oauthManager, oauthClient, authCode } = prepareTestInstances({ flow: OAuthFlow.U2M }); - const requestedScopes = []; + const requestedScopes: Array = []; authCode.expectedScope = getExpectedScope(requestedScopes); const testError = new Error('Test'); @@ -256,7 +291,7 @@ class OAuthClientMock { await oauthManager.refreshAccessToken(token); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(oauthClient.refresh.called).to.be.false; @@ -303,7 +338,14 @@ class OAuthClientMock { authCode.expectedScope = getExpectedScope([]); oauthClient.refresh.restore(); - sinon.stub(oauthClient, 'refresh').returns({}); + sinon.stub(oauthClient, 'refresh').returns( + Promise.resolve( + new TokenSet({ + access_token: undefined, + refresh_token: undefined, + }), + ), + ); try { const token = new OAuthToken(createExpiredAccessToken(), oauthClient.refreshToken); @@ -312,7 +354,7 @@ class OAuthClientMock { await oauthManager.refreshAccessToken(token); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(oauthClient.refresh.called).to.be.true; @@ -322,7 +364,7 @@ class OAuthClientMock { }); describe('M2M flow', () => { - function getExpectedScope(scopes) { + function getExpectedScope(scopes: Array) { switch (OAuthManagerClass) { case DatabricksOAuthManager: return [OAuthScope.allAPIs].join(scopeDelimiter); @@ -366,7 +408,7 @@ class OAuthClientMock { await oauthManager.getToken(requestedScopes); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(oauthClient.grant.called).to.be.true; @@ -380,7 +422,7 @@ class OAuthClientMock { clientId: 'test_client_id', clientSecret: 'test_client_secret', }); - const requestedScopes = []; + const requestedScopes: Array = []; oauthClient.expectedScope = getExpectedScope(requestedScopes); const testError = new Error('Test'); @@ -453,13 +495,20 @@ class OAuthClientMock { expect(token.hasExpired).to.be.true; oauthClient.grant.restore(); - sinon.stub(oauthClient, 'grant').returns({}); + sinon.stub(oauthClient, 'grant').returns( + Promise.resolve( + new TokenSet({ + access_token: undefined, + refresh_token: undefined, + }), + ), + ); try { await oauthManager.refreshAccessToken(token); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(oauthClient.grant.called).to.be.true; diff --git a/tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.js b/tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.ts similarity index 92% rename from tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.js rename to tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.ts index 6aaefea2..6ef217f3 100644 --- a/tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.js +++ b/tests/unit/connection/auth/DatabricksOAuth/OAuthToken.test.ts @@ -1,7 +1,7 @@ -const { expect } = require('chai'); -const OAuthToken = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken').default; +import { expect } from 'chai'; +import OAuthToken from '../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken'; -const { createAccessToken } = require('./utils'); +import { createAccessToken } from '../../../.stubs/OAuth'; describe('OAuthToken', () => { it('should be properly initialized', () => { diff --git a/tests/unit/connection/auth/DatabricksOAuth/index.test.js b/tests/unit/connection/auth/DatabricksOAuth/index.test.js deleted file mode 100644 index 3b9e7b51..00000000 --- a/tests/unit/connection/auth/DatabricksOAuth/index.test.js +++ /dev/null @@ -1,111 +0,0 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const DatabricksOAuth = require('../../../../../lib/connection/auth/DatabricksOAuth/index').default; -const OAuthToken = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken').default; -const OAuthManager = require('../../../../../lib/connection/auth/DatabricksOAuth/OAuthManager').default; - -const { createValidAccessToken, createExpiredAccessToken } = require('./utils'); - -class OAuthManagerMock { - constructor() { - this.getTokenResult = new OAuthToken(createValidAccessToken()); - this.refreshTokenResult = new OAuthToken(createValidAccessToken()); - } - - async refreshAccessToken(token) { - return token.hasExpired ? this.refreshTokenResult : token; - } - - async getToken() { - return this.getTokenResult; - } -} - -class OAuthPersistenceMock { - constructor() { - this.token = undefined; - - sinon.stub(this, 'persist').callThrough(); - sinon.stub(this, 'read').callThrough(); - } - - async persist(host, token) { - this.token = token; - } - - async read() { - return this.token; - } -} - -function prepareTestInstances(options) { - const oauthManager = new OAuthManagerMock(); - - sinon.stub(oauthManager, 'refreshAccessToken').callThrough(); - sinon.stub(oauthManager, 'getToken').callThrough(); - - sinon.stub(OAuthManager, 'getManager').returns(oauthManager); - - const provider = new DatabricksOAuth({ ...options }); - - return { oauthManager, provider }; -} - -describe('DatabricksOAuth', () => { - afterEach(() => { - OAuthManager.getManager.restore?.(); - }); - - it('should get persisted token if available', async () => { - const persistence = new OAuthPersistenceMock(); - persistence.token = new OAuthToken(createValidAccessToken()); - - const { provider } = prepareTestInstances({ persistence }); - - await provider.authenticate(); - expect(persistence.read.called).to.be.true; - }); - - it('should get new token if storage not available', async () => { - const { oauthManager, provider } = prepareTestInstances(); - - await provider.authenticate(); - expect(oauthManager.getToken.called).to.be.true; - }); - - it('should get new token if persisted token not available, and store valid token', async () => { - const persistence = new OAuthPersistenceMock(); - persistence.token = undefined; - const { oauthManager, provider } = prepareTestInstances({ persistence }); - - await provider.authenticate(); - expect(oauthManager.getToken.called).to.be.true; - expect(persistence.persist.called).to.be.true; - expect(persistence.token).to.be.equal(oauthManager.getTokenResult); - }); - - it('should refresh expired token and store new token', async () => { - const persistence = new OAuthPersistenceMock(); - persistence.token = undefined; - - const { oauthManager, provider } = prepareTestInstances({ persistence }); - oauthManager.getTokenResult = new OAuthToken(createExpiredAccessToken()); - oauthManager.refreshTokenResult = new OAuthToken(createValidAccessToken()); - - await provider.authenticate(); - expect(oauthManager.getToken.called).to.be.true; - expect(oauthManager.refreshAccessToken.called).to.be.true; - expect(oauthManager.refreshAccessToken.firstCall.firstArg).to.be.equal(oauthManager.getTokenResult); - expect(persistence.token).to.be.equal(oauthManager.refreshTokenResult); - expect(persistence.persist.called).to.be.true; - expect(persistence.token).to.be.equal(oauthManager.refreshTokenResult); - }); - - it('should configure transport using valid token', async () => { - const { oauthManager, provider } = prepareTestInstances(); - - const authHeaders = await provider.authenticate(); - expect(oauthManager.getToken.called).to.be.true; - expect(Object.keys(authHeaders)).to.deep.equal(['Authorization']); - }); -}); diff --git a/tests/unit/connection/auth/DatabricksOAuth/index.test.ts b/tests/unit/connection/auth/DatabricksOAuth/index.test.ts new file mode 100644 index 00000000..77c4d22f --- /dev/null +++ b/tests/unit/connection/auth/DatabricksOAuth/index.test.ts @@ -0,0 +1,101 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import DatabricksOAuth, { OAuthFlow } from '../../../../../lib/connection/auth/DatabricksOAuth'; +import OAuthToken from '../../../../../lib/connection/auth/DatabricksOAuth/OAuthToken'; + +import { + createExpiredAccessToken, + createValidAccessToken, + OAuthManagerStub, + OAuthPersistenceStub, +} from '../../../.stubs/OAuth'; +import ClientContextStub from '../../../.stubs/ClientContextStub'; + +const optionsStub = { + context: new ClientContextStub(), + flow: OAuthFlow.M2M, + host: 'localhost', +}; + +describe('DatabricksOAuth', () => { + it('should get persisted token if available', async () => { + const persistence = sinon.spy(new OAuthPersistenceStub()); + persistence.token = new OAuthToken(createValidAccessToken()); + + const options = { ...optionsStub, persistence }; + const provider = new DatabricksOAuth(options); + provider['manager'] = new OAuthManagerStub(options); + + await provider.authenticate(); + expect(persistence.read.called).to.be.true; + }); + + it('should get new token if storage not available', async () => { + const options = { ...optionsStub }; + + const oauthManager = new OAuthManagerStub(options); + const oauthManagerSpy = sinon.spy(oauthManager); + + const provider = new DatabricksOAuth(options); + provider['manager'] = oauthManager; + + await provider.authenticate(); + expect(oauthManagerSpy.getToken.called).to.be.true; + }); + + it('should get new token if persisted token not available, and store valid token', async () => { + const persistence = sinon.spy(new OAuthPersistenceStub()); + persistence.token = undefined; + + const options = { ...optionsStub, persistence }; + + const oauthManager = new OAuthManagerStub(options); + const oauthManagerSpy = sinon.spy(oauthManager); + + const provider = new DatabricksOAuth(options); + provider['manager'] = oauthManager; + + await provider.authenticate(); + expect(oauthManagerSpy.getToken.called).to.be.true; + expect(persistence.persist.called).to.be.true; + expect(persistence.token).to.be.equal(oauthManagerSpy.getTokenResult); + }); + + it('should refresh expired token and store new token', async () => { + const persistence = sinon.spy(new OAuthPersistenceStub()); + persistence.token = undefined; + + const options = { ...optionsStub, persistence }; + + const oauthManager = new OAuthManagerStub(options); + const oauthManagerSpy = sinon.spy(oauthManager); + + const provider = new DatabricksOAuth(options); + provider['manager'] = oauthManager; + + oauthManagerSpy.getTokenResult = new OAuthToken(createExpiredAccessToken()); + oauthManagerSpy.refreshTokenResult = new OAuthToken(createValidAccessToken()); + + await provider.authenticate(); + expect(oauthManagerSpy.getToken.called).to.be.true; + expect(oauthManagerSpy.refreshAccessToken.called).to.be.true; + expect(oauthManagerSpy.refreshAccessToken.firstCall.firstArg).to.be.equal(oauthManagerSpy.getTokenResult); + expect(persistence.token).to.be.equal(oauthManagerSpy.refreshTokenResult); + expect(persistence.persist.called).to.be.true; + expect(persistence.token).to.be.equal(oauthManagerSpy.refreshTokenResult); + }); + + it('should configure transport using valid token', async () => { + const options = { ...optionsStub }; + + const oauthManager = new OAuthManagerStub(options); + const oauthManagerSpy = sinon.spy(oauthManager); + + const provider = new DatabricksOAuth(options); + provider['manager'] = oauthManager; + + const authHeaders = await provider.authenticate(); + expect(oauthManagerSpy.getToken.called).to.be.true; + expect(Object.keys(authHeaders)).to.deep.equal(['Authorization']); + }); +}); diff --git a/tests/unit/connection/auth/DatabricksOAuth/utils.js b/tests/unit/connection/auth/DatabricksOAuth/utils.js deleted file mode 100644 index edffe7b1..00000000 --- a/tests/unit/connection/auth/DatabricksOAuth/utils.js +++ /dev/null @@ -1,20 +0,0 @@ -function createAccessToken(expirationTime) { - const payload = Buffer.from(JSON.stringify({ exp: expirationTime }), 'utf8').toString('base64'); - return `access.${payload}`; -} - -function createValidAccessToken() { - const expirationTime = Math.trunc(Date.now() / 1000) + 20000; - return createAccessToken(expirationTime); -} - -function createExpiredAccessToken() { - const expirationTime = Math.trunc(Date.now() / 1000) - 1000; - return createAccessToken(expirationTime); -} - -module.exports = { - createAccessToken, - createValidAccessToken, - createExpiredAccessToken, -}; diff --git a/tests/unit/connection/auth/PlainHttpAuthentication.test.js b/tests/unit/connection/auth/PlainHttpAuthentication.test.js deleted file mode 100644 index cf4ba927..00000000 --- a/tests/unit/connection/auth/PlainHttpAuthentication.test.js +++ /dev/null @@ -1,43 +0,0 @@ -const { expect } = require('chai'); -const PlainHttpAuthentication = require('../../../../lib/connection/auth/PlainHttpAuthentication').default; - -describe('PlainHttpAuthentication', () => { - it('username and password must be anonymous if nothing passed', () => { - const auth = new PlainHttpAuthentication({}); - - expect(auth.username).to.be.eq('anonymous'); - expect(auth.password).to.be.eq('anonymous'); - }); - - it('username and password must be defined correctly', () => { - const auth = new PlainHttpAuthentication({ - username: 'user', - password: 'pass', - }); - - expect(auth.username).to.be.eq('user'); - expect(auth.password).to.be.eq('pass'); - }); - - it('empty password must be set', () => { - const auth = new PlainHttpAuthentication({ - username: 'user', - password: '', - }); - - expect(auth.username).to.be.eq('user'); - expect(auth.password).to.be.eq(''); - }); - - it('auth token must be set to header', async () => { - const auth = new PlainHttpAuthentication({}); - const transportMock = { - updateHeaders(headers) { - expect(headers).to.deep.equal({ - Authorization: 'Bearer anonymous', - }); - }, - }; - await auth.authenticate(transportMock); // it just should not fail - }); -}); diff --git a/tests/unit/connection/auth/PlainHttpAuthentication.test.ts b/tests/unit/connection/auth/PlainHttpAuthentication.test.ts new file mode 100644 index 00000000..9b69029f --- /dev/null +++ b/tests/unit/connection/auth/PlainHttpAuthentication.test.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; +import PlainHttpAuthentication from '../../../../lib/connection/auth/PlainHttpAuthentication'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; + +describe('PlainHttpAuthentication', () => { + it('username and password must be anonymous if nothing passed', () => { + const auth = new PlainHttpAuthentication({ context: new ClientContextStub() }); + + expect(auth['username']).to.be.eq('anonymous'); + expect(auth['password']).to.be.eq('anonymous'); + }); + + it('username and password must be defined correctly', () => { + const auth = new PlainHttpAuthentication({ + context: new ClientContextStub(), + username: 'user', + password: 'pass', + }); + + expect(auth['username']).to.be.eq('user'); + expect(auth['password']).to.be.eq('pass'); + }); + + it('empty password must be set', () => { + const auth = new PlainHttpAuthentication({ + context: new ClientContextStub(), + username: 'user', + password: '', + }); + + expect(auth['username']).to.be.eq('user'); + expect(auth['password']).to.be.eq(''); + }); + + it('auth token must be set to header', async () => { + const auth = new PlainHttpAuthentication({ context: new ClientContextStub() }); + const headers = await auth.authenticate(); + expect(headers).to.deep.equal({ + Authorization: 'Bearer anonymous', + }); + }); +}); diff --git a/tests/unit/connection/connections/HttpConnection.test.js b/tests/unit/connection/connections/HttpConnection.test.ts similarity index 71% rename from tests/unit/connection/connections/HttpConnection.test.js rename to tests/unit/connection/connections/HttpConnection.test.ts index cc1cfb85..c7b39972 100644 --- a/tests/unit/connection/connections/HttpConnection.test.js +++ b/tests/unit/connection/connections/HttpConnection.test.ts @@ -1,24 +1,19 @@ -const http = require('http'); -const { expect } = require('chai'); -const HttpConnection = require('../../../../lib/connection/connections/HttpConnection').default; -const ThriftHttpConnection = require('../../../../lib/connection/connections/ThriftHttpConnection').default; -const DBSQLClient = require('../../../../lib/DBSQLClient').default; +import http from 'http'; +import { expect } from 'chai'; +import HttpConnection from '../../../../lib/connection/connections/HttpConnection'; +import ThriftHttpConnection from '../../../../lib/connection/connections/ThriftHttpConnection'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; describe('HttpConnection.connect', () => { it('should create Thrift connection', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; - const connection = new HttpConnection( { host: 'localhost', port: 10001, path: '/hive', }, - context, + new ClientContextStub(), ); const thriftConnection = await connection.getThriftConnection(); @@ -32,12 +27,6 @@ describe('HttpConnection.connect', () => { }); it('should set SSL certificates and disable rejectUnauthorized', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; - const connection = new HttpConnection( { host: 'localhost', @@ -48,7 +37,7 @@ describe('HttpConnection.connect', () => { cert: 'cert', key: 'key', }, - context, + new ClientContextStub(), ); const thriftConnection = await connection.getThriftConnection(); @@ -60,12 +49,6 @@ describe('HttpConnection.connect', () => { }); it('should initialize http agents', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; - const connection = new HttpConnection( { host: 'localhost', @@ -73,7 +56,7 @@ describe('HttpConnection.connect', () => { https: false, path: '/hive', }, - context, + new ClientContextStub(), ); const thriftConnection = await connection.getThriftConnection(); @@ -82,12 +65,6 @@ describe('HttpConnection.connect', () => { }); it('should update headers (case 1: Thrift connection not initialized)', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; - const initialHeaders = { a: 'test header A', b: 'test header B', @@ -100,7 +77,7 @@ describe('HttpConnection.connect', () => { path: '/hive', headers: initialHeaders, }, - context, + new ClientContextStub(), ); const extraHeaders = { @@ -108,7 +85,7 @@ describe('HttpConnection.connect', () => { c: 'test header C', }; connection.setHeaders(extraHeaders); - expect(connection.headers).to.deep.equal(extraHeaders); + expect(connection['headers']).to.deep.equal(extraHeaders); const thriftConnection = await connection.getThriftConnection(); @@ -119,12 +96,6 @@ describe('HttpConnection.connect', () => { }); it('should update headers (case 2: Thrift connection initialized)', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; - const initialHeaders = { a: 'test header A', b: 'test header B', @@ -137,12 +108,12 @@ describe('HttpConnection.connect', () => { path: '/hive', headers: initialHeaders, }, - context, + new ClientContextStub(), ); const thriftConnection = await connection.getThriftConnection(); - expect(connection.headers).to.deep.equal({}); + expect(connection['headers']).to.deep.equal({}); expect(thriftConnection.config.headers).to.deep.equal(initialHeaders); const extraHeaders = { @@ -150,7 +121,7 @@ describe('HttpConnection.connect', () => { c: 'test header C', }; connection.setHeaders(extraHeaders); - expect(connection.headers).to.deep.equal(extraHeaders); + expect(connection['headers']).to.deep.equal(extraHeaders); expect(thriftConnection.config.headers).to.deep.equal({ ...initialHeaders, ...extraHeaders, diff --git a/tests/unit/connection/connections/HttpRetryPolicy.test.js b/tests/unit/connection/connections/HttpRetryPolicy.test.ts similarity index 57% rename from tests/unit/connection/connections/HttpRetryPolicy.test.js rename to tests/unit/connection/connections/HttpRetryPolicy.test.ts index 881a4869..50ba0bf5 100644 --- a/tests/unit/connection/connections/HttpRetryPolicy.test.js +++ b/tests/unit/connection/connections/HttpRetryPolicy.test.ts @@ -1,43 +1,30 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const { Request, Response } = require('node-fetch'); -const HttpRetryPolicy = require('../../../../lib/connection/connections/HttpRetryPolicy').default; -const { default: RetryError, RetryErrorCode } = require('../../../../lib/errors/RetryError'); -const DBSQLClient = require('../../../../lib/DBSQLClient').default; - -class ClientContextMock { - constructor(configOverrides) { - this.configOverrides = configOverrides; - } - - getConfig() { - const defaultConfig = DBSQLClient.getDefaultConfig(); - return { - ...defaultConfig, - ...this.configOverrides, - }; - } -} +import { expect, AssertionError } from 'chai'; +import sinon from 'sinon'; +import { Request, Response, HeadersInit } from 'node-fetch'; +import HttpRetryPolicy from '../../../../lib/connection/connections/HttpRetryPolicy'; +import RetryError, { RetryErrorCode } from '../../../../lib/errors/RetryError'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; describe('HttpRetryPolicy', () => { it('should properly compute backoff delay', async () => { - const context = new ClientContextMock({ retryDelayMin: 3, retryDelayMax: 20 }); + const context = new ClientContextStub({ retryDelayMin: 3, retryDelayMax: 20 }); const { retryDelayMin, retryDelayMax } = context.getConfig(); const policy = new HttpRetryPolicy(context); - expect(policy.getBackoffDelay(0, retryDelayMin, retryDelayMax)).to.equal(3); - expect(policy.getBackoffDelay(1, retryDelayMin, retryDelayMax)).to.equal(6); - expect(policy.getBackoffDelay(2, retryDelayMin, retryDelayMax)).to.equal(12); - expect(policy.getBackoffDelay(3, retryDelayMin, retryDelayMax)).to.equal(retryDelayMax); - expect(policy.getBackoffDelay(4, retryDelayMin, retryDelayMax)).to.equal(retryDelayMax); + expect(policy['getBackoffDelay'](0, retryDelayMin, retryDelayMax)).to.equal(3); + expect(policy['getBackoffDelay'](1, retryDelayMin, retryDelayMax)).to.equal(6); + expect(policy['getBackoffDelay'](2, retryDelayMin, retryDelayMax)).to.equal(12); + expect(policy['getBackoffDelay'](3, retryDelayMin, retryDelayMax)).to.equal(retryDelayMax); + expect(policy['getBackoffDelay'](4, retryDelayMin, retryDelayMax)).to.equal(retryDelayMax); }); it('should extract delay from `Retry-After` header', async () => { - const context = new ClientContextMock({ retryDelayMin: 3, retryDelayMax: 20 }); + const context = new ClientContextStub({ retryDelayMin: 3, retryDelayMax: 20 }); const { retryDelayMin } = context.getConfig(); const policy = new HttpRetryPolicy(context); - function createMock(headers) { + function createStub(headers: HeadersInit) { return { request: new Request('http://localhost'), response: new Response(undefined, { headers }), @@ -45,26 +32,26 @@ describe('HttpRetryPolicy', () => { } // Missing `Retry-After` header - expect(policy.getRetryAfterHeader(createMock({}), retryDelayMin)).to.be.undefined; + expect(policy['getRetryAfterHeader'](createStub({}), retryDelayMin)).to.be.undefined; // Valid `Retry-After`, several header name variants - expect(policy.getRetryAfterHeader(createMock({ 'Retry-After': '10' }), retryDelayMin)).to.equal(10); - expect(policy.getRetryAfterHeader(createMock({ 'retry-after': '10' }), retryDelayMin)).to.equal(10); - expect(policy.getRetryAfterHeader(createMock({ 'RETRY-AFTER': '10' }), retryDelayMin)).to.equal(10); + expect(policy['getRetryAfterHeader'](createStub({ 'Retry-After': '10' }), retryDelayMin)).to.equal(10); + expect(policy['getRetryAfterHeader'](createStub({ 'retry-after': '10' }), retryDelayMin)).to.equal(10); + expect(policy['getRetryAfterHeader'](createStub({ 'RETRY-AFTER': '10' }), retryDelayMin)).to.equal(10); // Invalid header values (non-numeric, negative) - expect(policy.getRetryAfterHeader(createMock({ 'Retry-After': 'test' }), retryDelayMin)).to.be.undefined; - expect(policy.getRetryAfterHeader(createMock({ 'Retry-After': '-10' }), retryDelayMin)).to.be.undefined; + expect(policy['getRetryAfterHeader'](createStub({ 'Retry-After': 'test' }), retryDelayMin)).to.be.undefined; + expect(policy['getRetryAfterHeader'](createStub({ 'Retry-After': '-10' }), retryDelayMin)).to.be.undefined; // It should not be smaller than min value, but can be greater than max value - expect(policy.getRetryAfterHeader(createMock({ 'Retry-After': '1' }), retryDelayMin)).to.equal(retryDelayMin); - expect(policy.getRetryAfterHeader(createMock({ 'Retry-After': '200' }), retryDelayMin)).to.equal(200); + expect(policy['getRetryAfterHeader'](createStub({ 'Retry-After': '1' }), retryDelayMin)).to.equal(retryDelayMin); + expect(policy['getRetryAfterHeader'](createStub({ 'Retry-After': '200' }), retryDelayMin)).to.equal(200); }); it('should check if HTTP transaction is safe to retry', async () => { - const policy = new HttpRetryPolicy(new ClientContextMock()); + const policy = new HttpRetryPolicy(new ClientContextStub()); - function createMock(status) { + function createStub(status: number) { return { request: new Request('http://localhost'), response: new Response(undefined, { status }), @@ -73,30 +60,30 @@ describe('HttpRetryPolicy', () => { // Status codes below 100 can be retried for (let status = 1; status < 100; status += 1) { - expect(policy.isRetryable(createMock(status))).to.be.true; + expect(policy['isRetryable'](createStub(status))).to.be.true; } // Status codes between 100 (including) and 500 (excluding) should not be retried // The only exception is 429 (Too many requests) for (let status = 100; status < 500; status += 1) { const expectedResult = status === 429 ? true : false; - expect(policy.isRetryable(createMock(status))).to.equal(expectedResult); + expect(policy['isRetryable'](createStub(status))).to.equal(expectedResult); } // Status codes above 500 can be retried, except for 501 for (let status = 500; status < 1000; status += 1) { const expectedResult = status === 501 ? false : true; - expect(policy.isRetryable(createMock(status))).to.equal(expectedResult); + expect(policy['isRetryable'](createStub(status))).to.equal(expectedResult); } }); describe('shouldRetry', () => { it('should not retry if transaction succeeded', async () => { - const context = new ClientContextMock({ retryMaxAttempts: 3 }); + const context = new ClientContextStub({ retryMaxAttempts: 3 }); const clientConfig = context.getConfig(); const policy = new HttpRetryPolicy(context); - function createMock(status) { + function createStub(status: number) { return { request: new Request('http://localhost'), response: new Response(undefined, { status }), @@ -105,44 +92,50 @@ describe('HttpRetryPolicy', () => { // Try several times to make sure it doesn't increment an attempts counter for (let attempt = 1; attempt <= clientConfig.retryMaxAttempts + 1; attempt += 1) { - const result = await policy.shouldRetry(createMock(200)); + const result = await policy.shouldRetry(createStub(200)); expect(result.shouldRetry).to.be.false; - expect(policy.attempt).to.equal(0); + expect(policy['attempt']).to.equal(0); } // Make sure it doesn't trigger timeout when not needed - policy.startTime = Date.now() - clientConfig.retriesTimeout * 2; - const result = await policy.shouldRetry(createMock(200)); + policy['startTime'] = Date.now() - clientConfig.retriesTimeout * 2; + const result = await policy.shouldRetry(createStub(200)); expect(result.shouldRetry).to.be.false; }); it('should use `Retry-After` header as a base for backoff', async () => { - const context = new ClientContextMock({ retryDelayMin: 3, retryDelayMax: 100, retryMaxAttempts: 10 }); + const context = new ClientContextStub({ retryDelayMin: 3, retryDelayMax: 100, retryMaxAttempts: 10 }); const policy = new HttpRetryPolicy(context); - function createMock(headers) { + function createStub(headers: HeadersInit) { return { request: new Request('http://localhost'), response: new Response(undefined, { status: 500, headers }), }; } - const result1 = await policy.shouldRetry(createMock({ 'Retry-After': '5' })); + const result1 = await policy.shouldRetry(createStub({ 'Retry-After': '5' })); expect(result1.shouldRetry).to.be.true; - expect(result1.retryAfter).to.equal(10); + if (result1.shouldRetry) { + expect(result1.retryAfter).to.equal(10); + } - const result2 = await policy.shouldRetry(createMock({ 'Retry-After': '8' })); + const result2 = await policy.shouldRetry(createStub({ 'Retry-After': '8' })); expect(result2.shouldRetry).to.be.true; - expect(result2.retryAfter).to.equal(32); + if (result2.shouldRetry) { + expect(result2.retryAfter).to.equal(32); + } - policy.attempt = 4; - const result3 = await policy.shouldRetry(createMock({ 'Retry-After': '10' })); + policy['attempt'] = 4; + const result3 = await policy.shouldRetry(createStub({ 'Retry-After': '10' })); expect(result3.shouldRetry).to.be.true; - expect(result3.retryAfter).to.equal(100); + if (result3.shouldRetry) { + expect(result3.retryAfter).to.equal(100); + } }); it('should use backoff when `Retry-After` header is missing', async () => { - const context = new ClientContextMock({ + const context = new ClientContextStub({ retryDelayMin: 3, retryDelayMax: 20, retryMaxAttempts: Number.POSITIVE_INFINITY, // remove limit on max attempts @@ -150,58 +143,62 @@ describe('HttpRetryPolicy', () => { const clientConfig = context.getConfig(); const policy = new HttpRetryPolicy(context); - function createMock(headers) { + function createStub(headers: HeadersInit) { return { request: new Request('http://localhost'), response: new Response(undefined, { status: 500, headers }), }; } - const result1 = await policy.shouldRetry(createMock({})); + const result1 = await policy.shouldRetry(createStub({})); expect(result1.shouldRetry).to.be.true; - expect(result1.retryAfter).to.equal(6); + if (result1.shouldRetry) { + expect(result1.retryAfter).to.equal(6); + } - policy.attempt = 4; - const result2 = await policy.shouldRetry(createMock({ 'Retry-After': 'test' })); + policy['attempt'] = 4; + const result2 = await policy.shouldRetry(createStub({ 'Retry-After': 'test' })); expect(result2.shouldRetry).to.be.true; - expect(result2.retryAfter).to.equal(clientConfig.retryDelayMax); + if (result2.shouldRetry) { + expect(result2.retryAfter).to.equal(clientConfig.retryDelayMax); + } }); it('should check if retry timeout reached', async () => { - const context = new ClientContextMock(); + const context = new ClientContextStub(); const clientConfig = context.getConfig(); const policy = new HttpRetryPolicy(context); - function createMock() { + function createStub() { return { request: new Request('http://localhost', { method: 'POST' }), response: new Response(undefined, { status: 500 }), }; } - const result = await policy.shouldRetry(createMock()); + const result = await policy.shouldRetry(createStub()); expect(result.shouldRetry).to.be.true; // Modify start time to be in the past so the next `shouldRetry` would fail - policy.startTime = Date.now() - clientConfig.retriesTimeout * 2; + policy['startTime'] = Date.now() - clientConfig.retriesTimeout * 2; try { - await policy.shouldRetry(createMock()); + await policy.shouldRetry(createStub()); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceOf(RetryError); - expect(error.errorCode).to.equal(RetryErrorCode.TimeoutExceeded); + expect((error as RetryError).errorCode).to.equal(RetryErrorCode.TimeoutExceeded); } }); it('should check if retry attempts exceeded', async () => { - const context = new ClientContextMock({ retryMaxAttempts: 3 }); + const context = new ClientContextStub({ retryMaxAttempts: 3 }); const clientConfig = context.getConfig(); const policy = new HttpRetryPolicy(context); - function createMock() { + function createStub() { return { request: new Request('http://localhost', { method: 'POST' }), response: new Response(undefined, { status: 500 }), @@ -210,36 +207,34 @@ describe('HttpRetryPolicy', () => { // First attempts should succeed for (let attempt = 1; attempt < clientConfig.retryMaxAttempts; attempt += 1) { - const result = await policy.shouldRetry(createMock()); + const result = await policy.shouldRetry(createStub()); expect(result.shouldRetry).to.be.true; } // Modify start time to be in the past so the next `shouldRetry` would fail try { - await policy.shouldRetry(createMock()); + await policy.shouldRetry(createStub()); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceOf(RetryError); - expect(error.errorCode).to.equal(RetryErrorCode.AttemptsExceeded); + expect((error as RetryError).errorCode).to.equal(RetryErrorCode.AttemptsExceeded); } }); }); describe('invokeWithRetry', () => { it('should retry an operation until it succeeds', async () => { - const context = new ClientContextMock({ + const context = new ClientContextStub({ retryDelayMin: 1, retryDelayMax: 2, retryMaxAttempts: 20, }); - const policy = new HttpRetryPolicy(context); + const policy = sinon.spy(new HttpRetryPolicy(context)); - sinon.spy(policy, 'shouldRetry'); - - function createMock(status) { + function createStub(status: number) { return { request: new Request('http://localhost'), response: new Response(undefined, { status }), @@ -250,9 +245,9 @@ describe('HttpRetryPolicy', () => { const operation = sinon .stub() - .returns(createMock(500)) + .returns(createStub(500)) .onCall(expectedAttempts - 1) // call numbers are zero-based - .returns(createMock(200)); + .returns(createStub(200)); const result = await policy.invokeWithRetry(operation); expect(policy.shouldRetry.callCount).to.equal(expectedAttempts); @@ -261,17 +256,15 @@ describe('HttpRetryPolicy', () => { }); it('should stop retrying if retry limits reached', async () => { - const context = new ClientContextMock({ + const context = new ClientContextStub({ retryDelayMin: 1, retryDelayMax: 2, retryMaxAttempts: 3, }); const clientConfig = context.getConfig(); - const policy = new HttpRetryPolicy(context); - - sinon.spy(policy, 'shouldRetry'); + const policy = sinon.spy(new HttpRetryPolicy(context)); - function createMock(status) { + function createStub(status: number) { return { request: new Request('http://localhost'), response: new Response(undefined, { status }), @@ -280,7 +273,7 @@ describe('HttpRetryPolicy', () => { const expectedAttempts = clientConfig.retryMaxAttempts; - const operation = sinon.stub().returns(createMock(500)); + const operation = sinon.stub().returns(createStub(500)); try { await policy.invokeWithRetry(operation); diff --git a/tests/unit/connection/connections/NullRetryPolicy.test.js b/tests/unit/connection/connections/NullRetryPolicy.test.ts similarity index 80% rename from tests/unit/connection/connections/NullRetryPolicy.test.js rename to tests/unit/connection/connections/NullRetryPolicy.test.ts index e0ad8b79..a2fb49b1 100644 --- a/tests/unit/connection/connections/NullRetryPolicy.test.js +++ b/tests/unit/connection/connections/NullRetryPolicy.test.ts @@ -1,6 +1,6 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const NullRetryPolicy = require('../../../../lib/connection/connections/NullRetryPolicy').default; +import { expect, AssertionError } from 'chai'; +import sinon from 'sinon'; +import NullRetryPolicy from '../../../../lib/connection/connections/NullRetryPolicy'; describe('NullRetryPolicy', () => { it('should never allow retries', async () => { diff --git a/tests/unit/dto/InfoValue.test.js b/tests/unit/dto/InfoValue.test.js deleted file mode 100644 index 94e962c1..00000000 --- a/tests/unit/dto/InfoValue.test.js +++ /dev/null @@ -1,70 +0,0 @@ -const { expect } = require('chai'); -const InfoValue = require('../../../lib/dto/InfoValue').default; -const NodeInt64 = require('node-int64'); - -const createInfoValueMock = (value) => - Object.assign( - { - stringValue: null, - smallIntValue: null, - integerBitmask: null, - integerFlag: null, - lenValue: null, - }, - value, - ); - -describe('InfoValue', () => { - it('should return string', () => { - const value = new InfoValue( - createInfoValueMock({ - stringValue: 'value', - }), - ); - - expect(value.getValue()).to.be.eq('value'); - }); - - it('should return number', () => { - const smallInt = new InfoValue( - createInfoValueMock({ - smallIntValue: 1, - }), - ); - - expect(smallInt.getValue()).to.be.eq(1); - - const bitMask = new InfoValue( - createInfoValueMock({ - integerBitmask: 0xaa55aa55, - }), - ); - - expect(bitMask.getValue()).to.be.eq(0xaa55aa55); - - const integerFlag = new InfoValue( - createInfoValueMock({ - integerFlag: 0x01, - }), - ); - - expect(integerFlag.getValue()).to.be.eq(0x01); - }); - - it('should return int64', () => { - const value = new InfoValue( - createInfoValueMock({ - lenValue: new NodeInt64(Buffer.from([0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10])), - }), - ); - - expect(value.getValue()).to.be.instanceOf(NodeInt64); - expect(value.getValue().toNumber()).to.be.eq(4521260802379792); - }); - - it('should return null for empty info value', () => { - const value = new InfoValue(createInfoValueMock({})); - - expect(value.getValue()).to.be.null; - }); -}); diff --git a/tests/unit/dto/InfoValue.test.ts b/tests/unit/dto/InfoValue.test.ts new file mode 100644 index 00000000..d5b42ed4 --- /dev/null +++ b/tests/unit/dto/InfoValue.test.ts @@ -0,0 +1,48 @@ +import { expect } from 'chai'; +import Int64 from 'node-int64'; +import InfoValue from '../../../lib/dto/InfoValue'; + +describe('InfoValue', () => { + it('should return string', () => { + const value = new InfoValue({ + stringValue: 'value', + }); + + expect(value.getValue()).to.be.eq('value'); + }); + + it('should return number', () => { + const smallInt = new InfoValue({ + smallIntValue: 1, + }); + + expect(smallInt.getValue()).to.be.eq(1); + + const bitMask = new InfoValue({ + integerBitmask: 0xaa55aa55, + }); + + expect(bitMask.getValue()).to.be.eq(0xaa55aa55); + + const integerFlag = new InfoValue({ + integerFlag: 0x01, + }); + + expect(integerFlag.getValue()).to.be.eq(0x01); + }); + + it('should return int64', () => { + const value = new InfoValue({ + lenValue: new Int64(Buffer.from([0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10])), + }); + + expect(value.getValue()).to.be.instanceOf(Int64); + expect(value.getValue()?.toString()).to.be.eq('4521260802379792'); + }); + + it('should return null for empty info value', () => { + const value = new InfoValue({}); + + expect(value.getValue()).to.be.null; + }); +}); diff --git a/tests/unit/dto/Status.test.js b/tests/unit/dto/Status.test.ts similarity index 76% rename from tests/unit/dto/Status.test.js rename to tests/unit/dto/Status.test.ts index e37c4645..076c0c69 100644 --- a/tests/unit/dto/Status.test.js +++ b/tests/unit/dto/Status.test.ts @@ -1,11 +1,11 @@ -const { expect } = require('chai'); -const { TCLIService_types } = require('../../../lib').thrift; -const Status = require('../../../lib/dto/Status').default; +import { expect } from 'chai'; +import { TStatusCode } from '../../../thrift/TCLIService_types'; +import Status from '../../../lib/dto/Status'; describe('StatusFactory', () => { it('should be success', () => { const status = new Status({ - statusCode: TCLIService_types.TStatusCode.SUCCESS_STATUS, + statusCode: TStatusCode.SUCCESS_STATUS, }); expect(status.isSuccess).to.be.true; @@ -16,7 +16,7 @@ describe('StatusFactory', () => { it('should be success and have info messages', () => { const status = new Status({ - statusCode: TCLIService_types.TStatusCode.SUCCESS_WITH_INFO_STATUS, + statusCode: TStatusCode.SUCCESS_WITH_INFO_STATUS, infoMessages: ['message1', 'message2'], }); @@ -28,7 +28,7 @@ describe('StatusFactory', () => { it('should be executing', () => { const status = new Status({ - statusCode: TCLIService_types.TStatusCode.STILL_EXECUTING_STATUS, + statusCode: TStatusCode.STILL_EXECUTING_STATUS, }); expect(status.isSuccess).to.be.false; @@ -38,14 +38,11 @@ describe('StatusFactory', () => { }); it('should be error', () => { - const statusCodes = [ - TCLIService_types.TStatusCode.ERROR_STATUS, - TCLIService_types.TStatusCode.INVALID_HANDLE_STATUS, - ]; + const statusCodes = [TStatusCode.ERROR_STATUS, TStatusCode.INVALID_HANDLE_STATUS]; for (const statusCode of statusCodes) { const status = new Status({ - statusCode: TCLIService_types.TStatusCode.ERROR_STATUS, + statusCode: TStatusCode.ERROR_STATUS, }); expect(status.isSuccess).to.be.false; @@ -77,7 +74,7 @@ describe('StatusFactory', () => { it('should throw exception on error status', () => { const error = expect(() => { Status.assert({ - statusCode: TCLIService_types.TStatusCode.ERROR_STATUS, + statusCode: TStatusCode.ERROR_STATUS, errorMessage: 'error', errorCode: 1, infoMessages: ['line1', 'line2'], @@ -91,7 +88,7 @@ describe('StatusFactory', () => { it('should throw exception on invalid handle status', () => { const error = expect(() => { Status.assert({ - statusCode: TCLIService_types.TStatusCode.INVALID_HANDLE_STATUS, + statusCode: TStatusCode.INVALID_HANDLE_STATUS, errorMessage: 'error', }); }).to.throw('error'); @@ -102,9 +99,9 @@ describe('StatusFactory', () => { it('should not throw exception on success and execution status', () => { const statusCodes = [ - TCLIService_types.TStatusCode.SUCCESS_STATUS, - TCLIService_types.TStatusCode.SUCCESS_WITH_INFO_STATUS, - TCLIService_types.TStatusCode.STILL_EXECUTING_STATUS, + TStatusCode.SUCCESS_STATUS, + TStatusCode.SUCCESS_WITH_INFO_STATUS, + TStatusCode.STILL_EXECUTING_STATUS, ]; for (const statusCode of statusCodes) { diff --git a/tests/unit/hive/HiveDriver.test.js b/tests/unit/hive/HiveDriver.test.js deleted file mode 100644 index d2064880..00000000 --- a/tests/unit/hive/HiveDriver.test.js +++ /dev/null @@ -1,115 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const { TCLIService_types } = require('../../../lib').thrift; -const HiveDriver = require('../../../lib/hive/HiveDriver').default; - -const toTitleCase = (str) => str[0].toUpperCase() + str.slice(1); - -const testCommand = async (command, request) => { - const client = {}; - const clientContext = { - getClient: sinon.stub().returns(Promise.resolve(client)), - }; - const driver = new HiveDriver({ - context: clientContext, - }); - - const response = { response: 'value' }; - client[toTitleCase(command)] = function (req, cb) { - expect(req).to.be.deep.eq(new TCLIService_types[`T${toTitleCase(command)}Req`](request)); - cb(null, response); - }; - - const resp = await driver[command](request); - expect(resp).to.be.deep.eq(response); - expect(clientContext.getClient.called).to.be.true; -}; - -describe('HiveDriver', () => { - const sessionHandle = { sessionId: { guid: 'guid', secret: 'secret' } }; - const operationHandle = { - operationId: { guid: 'guid', secret: 'secret' }, - operationType: '', - hasResultSet: false, - }; - - it('should execute closeSession', () => { - return testCommand('closeSession', { sessionHandle }); - }); - - it('should execute executeStatement', () => { - return testCommand('executeStatement', { sessionHandle, statement: 'SELECT * FROM t' }); - }); - - it('should execute getResultSetMetadata', () => { - return testCommand('getResultSetMetadata', { operationHandle }); - }); - - it('should execute fetchResults', () => { - return testCommand('fetchResults', { operationHandle, orientation: 1, maxRows: 100 }); - }); - - it('should execute getInfo', () => { - return testCommand('getInfo', { sessionHandle, infoType: 1 }); - }); - - it('should execute getTypeInfo', () => { - return testCommand('getTypeInfo', { sessionHandle }); - }); - - it('should execute getCatalogs', () => { - return testCommand('getCatalogs', { sessionHandle }); - }); - - it('should execute getSchemas', () => { - return testCommand('getSchemas', { sessionHandle }); - }); - - it('should execute getTables', () => { - return testCommand('getTables', { sessionHandle }); - }); - - it('should execute getTableTypes', () => { - return testCommand('getTableTypes', { sessionHandle }); - }); - - it('should execute getColumns', () => { - return testCommand('getColumns', { sessionHandle }); - }); - - it('should execute getFunctions', () => { - return testCommand('getFunctions', { sessionHandle, functionName: 'AVG' }); - }); - - it('should execute getPrimaryKeys', () => { - return testCommand('getPrimaryKeys', { sessionHandle }); - }); - - it('should execute getCrossReference', () => { - return testCommand('getCrossReference', { sessionHandle }); - }); - - it('should execute getOperationStatus', () => { - return testCommand('getOperationStatus', { operationHandle }); - }); - - it('should execute cancelOperation', () => { - return testCommand('cancelOperation', { operationHandle }); - }); - - it('should execute closeOperation', () => { - return testCommand('closeOperation', { operationHandle }); - }); - - it('should execute getDelegationToken', () => { - return testCommand('getDelegationToken', { sessionHandle, owner: 'owner', renewer: 'renewer' }); - }); - - it('should execute cancelDelegationToken', () => { - return testCommand('cancelDelegationToken', { sessionHandle, delegationToken: 'delegationToken' }); - }); - - it('should execute renewDelegationToken', () => { - return testCommand('renewDelegationToken', { sessionHandle, delegationToken: 'delegationToken' }); - }); -}); diff --git a/tests/unit/hive/HiveDriver.test.ts b/tests/unit/hive/HiveDriver.test.ts new file mode 100644 index 00000000..4a45d885 --- /dev/null +++ b/tests/unit/hive/HiveDriver.test.ts @@ -0,0 +1,336 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import Int64 from 'node-int64'; +import HiveDriver from '../../../lib/hive/HiveDriver'; +import { + TCancelDelegationTokenReq, + TCancelOperationReq, + TCloseOperationReq, + TCloseSessionReq, + TExecuteStatementReq, + TFetchOrientation, + TFetchResultsReq, + TGetCatalogsReq, + TGetColumnsReq, + TGetCrossReferenceReq, + TGetDelegationTokenReq, + TGetFunctionsReq, + TGetInfoReq, + TGetInfoType, + TGetOperationStatusReq, + TGetPrimaryKeysReq, + TGetResultSetMetadataReq, + TGetSchemasReq, + TGetTablesReq, + TGetTableTypesReq, + TGetTypeInfoReq, + TOperationHandle, + TOperationType, + TRenewDelegationTokenReq, + TSessionHandle, +} from '../../../thrift/TCLIService_types'; + +import ClientContextStub from '../.stubs/ClientContextStub'; + +describe('HiveDriver', () => { + const sessionHandle: TSessionHandle = { sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) } }; + + const operationHandle: TOperationHandle = { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.UNKNOWN, + hasResultSet: false, + }; + + it('should execute closeSession', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TCloseSessionReq = { sessionHandle }; + const response = await driver.closeSession(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.CloseSession.called).to.be.true; + expect(new TCloseSessionReq(request)).to.deep.equal(context.thriftClient.closeSessionReq); + expect(response).to.deep.equal(context.thriftClient.closeSessionResp); + }); + + it('should execute executeStatement', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TExecuteStatementReq = { sessionHandle, statement: 'SELECT 1' }; + const response = await driver.executeStatement(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.ExecuteStatement.called).to.be.true; + expect(new TExecuteStatementReq(request)).to.deep.equal(context.thriftClient.executeStatementReq); + expect(response).to.deep.equal(context.thriftClient.executeStatementResp); + }); + + it('should execute getResultSetMetadata', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetResultSetMetadataReq = { operationHandle }; + const response = await driver.getResultSetMetadata(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetResultSetMetadata.called).to.be.true; + expect(new TGetResultSetMetadataReq(request)).to.deep.equal(context.thriftClient.getResultSetMetadataReq); + expect(response).to.deep.equal(context.thriftClient.getResultSetMetadataResp); + }); + + it('should execute fetchResults', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TFetchResultsReq = { + operationHandle, + orientation: TFetchOrientation.FETCH_FIRST, + maxRows: new Int64(1), + }; + const response = await driver.fetchResults(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.FetchResults.called).to.be.true; + expect(new TFetchResultsReq(request)).to.deep.equal(context.thriftClient.fetchResultsReq); + expect(response).to.deep.equal(context.thriftClient.fetchResultsResp); + }); + + it('should execute getInfo', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetInfoReq = { sessionHandle, infoType: TGetInfoType.CLI_SERVER_NAME }; + const response = await driver.getInfo(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetInfo.called).to.be.true; + expect(new TGetInfoReq(request)).to.deep.equal(context.thriftClient.getInfoReq); + expect(response).to.deep.equal(context.thriftClient.getInfoResp); + }); + + it('should execute getTypeInfo', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetTypeInfoReq = { sessionHandle }; + const response = await driver.getTypeInfo(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetTypeInfo.called).to.be.true; + expect(new TGetTypeInfoReq(request)).to.deep.equal(context.thriftClient.getTypeInfoReq); + expect(response).to.deep.equal(context.thriftClient.getTypeInfoResp); + }); + + it('should execute getCatalogs', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetCatalogsReq = { sessionHandle }; + const response = await driver.getCatalogs(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetCatalogs.called).to.be.true; + expect(new TGetCatalogsReq(request)).to.deep.equal(context.thriftClient.getCatalogsReq); + expect(response).to.deep.equal(context.thriftClient.getCatalogsResp); + }); + + it('should execute getSchemas', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetSchemasReq = { sessionHandle, catalogName: 'catalog' }; + const response = await driver.getSchemas(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetSchemas.called).to.be.true; + expect(new TGetSchemasReq(request)).to.deep.equal(context.thriftClient.getSchemasReq); + expect(response).to.deep.equal(context.thriftClient.getSchemasResp); + }); + + it('should execute getTables', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetTablesReq = { sessionHandle, catalogName: 'catalog', schemaName: 'schema' }; + const response = await driver.getTables(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetTables.called).to.be.true; + expect(new TGetTablesReq(request)).to.deep.equal(context.thriftClient.getTablesReq); + expect(response).to.deep.equal(context.thriftClient.getTablesResp); + }); + + it('should execute getTableTypes', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetTableTypesReq = { sessionHandle }; + const response = await driver.getTableTypes(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetTableTypes.called).to.be.true; + expect(new TGetTableTypesReq(request)).to.deep.equal(context.thriftClient.getTableTypesReq); + expect(response).to.deep.equal(context.thriftClient.getTableTypesResp); + }); + + it('should execute getColumns', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetColumnsReq = { sessionHandle, catalogName: 'catalog', schemaName: 'schema', tableName: 'table' }; + const response = await driver.getColumns(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetColumns.called).to.be.true; + expect(new TGetColumnsReq(request)).to.deep.equal(context.thriftClient.getColumnsReq); + expect(response).to.deep.equal(context.thriftClient.getColumnsResp); + }); + + it('should execute getFunctions', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetFunctionsReq = { + sessionHandle, + catalogName: 'catalog', + schemaName: 'schema', + functionName: 'func', + }; + const response = await driver.getFunctions(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetFunctions.called).to.be.true; + expect(new TGetFunctionsReq(request)).to.deep.equal(context.thriftClient.getFunctionsReq); + expect(response).to.deep.equal(context.thriftClient.getFunctionsResp); + }); + + it('should execute getPrimaryKeys', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetPrimaryKeysReq = { sessionHandle, catalogName: 'catalog', schemaName: 'schema' }; + const response = await driver.getPrimaryKeys(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetPrimaryKeys.called).to.be.true; + expect(new TGetPrimaryKeysReq(request)).to.deep.equal(context.thriftClient.getPrimaryKeysReq); + expect(response).to.deep.equal(context.thriftClient.getPrimaryKeysResp); + }); + + it('should execute getCrossReference', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetCrossReferenceReq = { + sessionHandle, + parentCatalogName: 'parent_catalog', + foreignCatalogName: 'foreign_catalog', + }; + const response = await driver.getCrossReference(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetCrossReference.called).to.be.true; + expect(new TGetCrossReferenceReq(request)).to.deep.equal(context.thriftClient.getCrossReferenceReq); + expect(response).to.deep.equal(context.thriftClient.getCrossReferenceResp); + }); + + it('should execute getOperationStatus', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetOperationStatusReq = { operationHandle }; + const response = await driver.getOperationStatus(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetOperationStatus.called).to.be.true; + expect(new TGetOperationStatusReq(request)).to.deep.equal(context.thriftClient.getOperationStatusReq); + expect(response).to.deep.equal(context.thriftClient.getOperationStatusResp); + }); + + it('should execute cancelOperation', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TCancelOperationReq = { operationHandle }; + const response = await driver.cancelOperation(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.CancelOperation.called).to.be.true; + expect(new TCancelOperationReq(request)).to.deep.equal(context.thriftClient.cancelOperationReq); + expect(response).to.deep.equal(context.thriftClient.cancelOperationResp); + }); + + it('should execute closeOperation', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TCloseOperationReq = { operationHandle }; + const response = await driver.closeOperation(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.CloseOperation.called).to.be.true; + expect(new TCloseOperationReq(request)).to.deep.equal(context.thriftClient.closeOperationReq); + expect(response).to.deep.equal(context.thriftClient.closeOperationResp); + }); + + it('should execute getDelegationToken', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TGetDelegationTokenReq = { sessionHandle, owner: 'owner', renewer: 'renewer' }; + const response = await driver.getDelegationToken(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.GetDelegationToken.called).to.be.true; + expect(new TGetDelegationTokenReq(request)).to.deep.equal(context.thriftClient.getDelegationTokenReq); + expect(response).to.deep.equal(context.thriftClient.getDelegationTokenResp); + }); + + it('should execute cancelDelegationToken', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TCancelDelegationTokenReq = { sessionHandle, delegationToken: 'token' }; + const response = await driver.cancelDelegationToken(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.CancelDelegationToken.called).to.be.true; + expect(new TCancelDelegationTokenReq(request)).to.deep.equal(context.thriftClient.cancelDelegationTokenReq); + expect(response).to.deep.equal(context.thriftClient.cancelDelegationTokenResp); + }); + + it('should execute renewDelegationToken', async () => { + const context = sinon.spy(new ClientContextStub()); + const thriftClient = sinon.spy(context.thriftClient); + const driver = new HiveDriver({ context }); + + const request: TRenewDelegationTokenReq = { sessionHandle, delegationToken: 'token' }; + const response = await driver.renewDelegationToken(request); + + expect(context.getClient.called).to.be.true; + expect(thriftClient.RenewDelegationToken.called).to.be.true; + expect(new TRenewDelegationTokenReq(request)).to.deep.equal(context.thriftClient.renewDelegationTokenReq); + expect(response).to.deep.equal(context.thriftClient.renewDelegationTokenResp); + }); +}); diff --git a/tests/unit/hive/commands/BaseCommand.test.js b/tests/unit/hive/commands/BaseCommand.test.ts similarity index 52% rename from tests/unit/hive/commands/BaseCommand.test.js rename to tests/unit/hive/commands/BaseCommand.test.ts index a21bd9cd..b1514775 100644 --- a/tests/unit/hive/commands/BaseCommand.test.js +++ b/tests/unit/hive/commands/BaseCommand.test.ts @@ -1,63 +1,72 @@ -const { expect, AssertionError } = require('chai'); -const { Request, Response } = require('node-fetch'); -const { Thrift } = require('thrift'); -const HiveDriverError = require('../../../../lib/errors/HiveDriverError').default; -const BaseCommand = require('../../../../lib/hive/Commands/BaseCommand').default; -const HttpRetryPolicy = require('../../../../lib/connection/connections/HttpRetryPolicy').default; -const DBSQLClient = require('../../../../lib/DBSQLClient').default; - -class ThriftClientMock { - constructor(context, methodHandler) { +import { expect, AssertionError } from 'chai'; +import sinon from 'sinon'; +import { Request, Response } from 'node-fetch'; +import { Thrift } from 'thrift'; +import HiveDriverError from '../../../../lib/errors/HiveDriverError'; +import BaseCommand from '../../../../lib/hive/Commands/BaseCommand'; +import HttpRetryPolicy from '../../../../lib/connection/connections/HttpRetryPolicy'; +import { THTTPException } from '../../../../lib/connection/connections/ThriftHttpConnection'; +import { HttpTransactionDetails } from '../../../../lib/connection/contracts/IConnectionProvider'; +import IClientContext from '../../../../lib/contracts/IClientContext'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; + +class TCustomReq {} + +class TCustomResp {} + +class ThriftClientStub { + static defaultResponse = { + status: { statusCode: 0 }, + }; + + private readonly context: IClientContext; + + private readonly methodHandler: () => Promise; + + constructor(context: IClientContext, methodHandler: () => Promise) { this.context = context; this.methodHandler = methodHandler; } - CustomMethod(request, callback) { + CustomMethod(req: TCustomReq, callback?: (error: any, resp?: TCustomResp) => void) { try { const retryPolicy = new HttpRetryPolicy(this.context); retryPolicy .invokeWithRetry(this.methodHandler) + .then(({ response }) => response.json()) .then((response) => { - callback(undefined, response?.body ?? ThriftClientMock.defaultResponse); + callback?.(undefined, response); }) - .catch((error) => { - callback(error); + .catch?.((error) => { + callback?.(error, undefined); }); } catch (error) { - callback(error); + callback?.(error, undefined); } } } -ThriftClientMock.defaultResponse = { - status: { statusCode: 0 }, -}; - -class CustomCommand extends BaseCommand { - constructor(...args) { - super(...args); - } - - execute(request) { - return this.executeCommand(request, this.client.CustomMethod); +class CustomCommand extends BaseCommand { + public async execute(request: TCustomReq): Promise { + return this.executeCommand(request, this.client.CustomMethod); } } describe('BaseCommand', () => { it('should fail if trying to invoke non-existing command', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub(); - const command = new CustomCommand({}, context); + // Here we have a special test condition - when invalid Thrift client is passed to + // a command. Normally TS should catch this (and therefore we have a type cast here), + // but there is an additional check in the code, which we need to verify as well + const command = new CustomCommand({} as ThriftClientStub, context); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(HiveDriverError); @@ -68,26 +77,22 @@ describe('BaseCommand', () => { it('should handle exceptions thrown by command', async () => { const errorMessage = 'Unexpected error'; - const clientConfig = DBSQLClient.getDefaultConfig(); + const context = new ClientContextStub(); - const context = { - getConfig: () => clientConfig, - }; + const thriftClient = new ThriftClientStub(context, async () => { + throw new Error('Not implemented'); + }); + sinon.stub(thriftClient, 'CustomMethod').callsFake(() => { + throw new Error(errorMessage); + }); - const command = new CustomCommand( - { - CustomMethod() { - throw new Error(errorMessage); - }, - }, - context, - ); + const command = new CustomCommand(thriftClient, context); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(Error); @@ -98,20 +103,16 @@ describe('BaseCommand', () => { [429, 503].forEach((statusCode) => { describe(`HTTP ${statusCode} error`, () => { it('should fail on max retry attempts exceeded', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - clientConfig.retriesTimeout = 200; // ms - clientConfig.retryDelayMin = 5; // ms - clientConfig.retryDelayMax = 20; // ms - clientConfig.retryMaxAttempts = 3; - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub({ + retriesTimeout: 200, // ms + retryDelayMin: 5, // ms + retryDelayMax: 20, // ms + retryMaxAttempts: 3, + }); let methodCallCount = 0; const command = new CustomCommand( - new ThriftClientMock(context, () => { + new ThriftClientStub(context, async () => { methodCallCount += 1; const request = new Request('http://localhost/', { method: 'POST' }); const response = new Response(undefined, { @@ -123,34 +124,30 @@ describe('BaseCommand', () => { ); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(HiveDriverError); expect(error.message).to.contain(`${statusCode} when connecting to resource`); expect(error.message).to.contain('Max retry count exceeded'); - expect(methodCallCount).to.equal(clientConfig.retryMaxAttempts); + expect(methodCallCount).to.equal(context.getConfig().retryMaxAttempts); } }); it('should fail on retry timeout exceeded', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - clientConfig.retriesTimeout = 200; // ms - clientConfig.retryDelayMin = 5; // ms - clientConfig.retryDelayMax = 20; // ms - clientConfig.retryMaxAttempts = 50; - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub({ + retriesTimeout: 200, // ms + retryDelayMin: 5, // ms + retryDelayMax: 20, // ms + retryMaxAttempts: 50, + }); let methodCallCount = 0; const command = new CustomCommand( - new ThriftClientMock(context, () => { + new ThriftClientStub(context, async () => { methodCallCount += 1; const request = new Request('http://localhost/', { method: 'POST' }); const response = new Response(undefined, { @@ -162,10 +159,10 @@ describe('BaseCommand', () => { ); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(HiveDriverError); @@ -179,20 +176,16 @@ describe('BaseCommand', () => { }); it('should succeed after few attempts', async () => { - const clientConfig = DBSQLClient.getDefaultConfig(); - - clientConfig.retriesTimeout = 200; // ms - clientConfig.retryDelayMin = 5; // ms - clientConfig.retryDelayMax = 20; // ms - clientConfig.retryMaxAttempts = 5; - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub({ + retriesTimeout: 200, // ms + retryDelayMin: 5, // ms + retryDelayMax: 20, // ms + retryMaxAttempts: 5, + }); let methodCallCount = 0; const command = new CustomCommand( - new ThriftClientMock(context, () => { + new ThriftClientStub(context, async () => { const request = new Request('http://localhost/', { method: 'POST' }); methodCallCount += 1; @@ -203,73 +196,64 @@ describe('BaseCommand', () => { return { request, response }; } - const response = new Response(undefined, { + const response = new Response(JSON.stringify(ThriftClientStub.defaultResponse), { status: 200, }); - response.body = ThriftClientMock.defaultResponse; return { request, response }; }), context, ); - const response = await command.execute(); - expect(response).to.deep.equal(ThriftClientMock.defaultResponse); + const response = await command.execute({}); + expect(response).to.deep.equal(ThriftClientStub.defaultResponse); expect(methodCallCount).to.equal(4); // 3 failed attempts + 1 succeeded }); }); }); it(`should re-throw unrecognized HTTP errors`, async () => { - const errorMessage = 'Unrecognized HTTP error'; - - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub(); const command = new CustomCommand( - new ThriftClientMock(context, () => { - const error = new Thrift.TApplicationException(undefined, errorMessage); - error.statusCode = 500; - throw error; + new ThriftClientStub(context, async () => { + throw new THTTPException( + new Response(undefined, { + status: 500, + }), + ); }), context, ); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(Thrift.TApplicationException); - expect(error.message).to.contain(errorMessage); + expect(error.message).to.contain('bad HTTP status code'); } }); it(`should re-throw unrecognized Thrift errors`, async () => { const errorMessage = 'Unrecognized HTTP error'; - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub(); const command = new CustomCommand( - new ThriftClientMock(context, () => { + new ThriftClientStub(context, async () => { throw new Thrift.TApplicationException(undefined, errorMessage); }), context, ); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error).to.be.instanceof(Thrift.TApplicationException); @@ -280,27 +264,22 @@ describe('BaseCommand', () => { it(`should re-throw unrecognized errors`, async () => { const errorMessage = 'Unrecognized error'; - const clientConfig = DBSQLClient.getDefaultConfig(); - - const context = { - getConfig: () => clientConfig, - }; + const context = new ClientContextStub(); const command = new CustomCommand( - new ThriftClientMock(context, () => { + new ThriftClientStub(context, async () => { throw new Error(errorMessage); }), context, ); try { - await command.execute(); + await command.execute({}); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } - expect(error).to.be.instanceof(Error); expect(error.message).to.contain(errorMessage); } }); diff --git a/tests/unit/hive/commands/CancelDelegationTokenCommand.test.js b/tests/unit/hive/commands/CancelDelegationTokenCommand.test.js deleted file mode 100644 index cb14cc0c..00000000 --- a/tests/unit/hive/commands/CancelDelegationTokenCommand.test.js +++ /dev/null @@ -1,54 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const CancelDelegationTokenCommand = require('../../../../lib/hive/Commands/CancelDelegationTokenCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - delegationToken: 'token', -}; - -const responseMock = { - status: { statusCode: 0 }, -}; - -function TCancelDelegationTokenReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - CancelDelegationToken(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('CancelDelegationTokenCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TCancelDelegationTokenReq', TCancelDelegationTokenReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new CancelDelegationTokenCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/CancelDelegationTokenCommand.test.ts b/tests/unit/hive/commands/CancelDelegationTokenCommand.test.ts new file mode 100644 index 00000000..dd11c6f9 --- /dev/null +++ b/tests/unit/hive/commands/CancelDelegationTokenCommand.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import CancelDelegationTokenCommand from '../../../../lib/hive/Commands/CancelDelegationTokenCommand'; +import { TCancelDelegationTokenReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('CancelDelegationTokenCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new CancelDelegationTokenCommand(thriftClient, new ClientContextStub()); + + const request: TCancelDelegationTokenReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + delegationToken: 'token', + }; + + const response = await command.execute(request); + expect(thriftClient.CancelDelegationToken.called).to.be.true; + expect(thriftClient.cancelDelegationTokenReq).to.deep.equal(new TCancelDelegationTokenReq(request)); + expect(response).to.be.deep.eq(thriftClient.cancelDelegationTokenResp); + }); +}); diff --git a/tests/unit/hive/commands/CancelOperationCommand.test.js b/tests/unit/hive/commands/CancelOperationCommand.test.js deleted file mode 100644 index 94f06a50..00000000 --- a/tests/unit/hive/commands/CancelOperationCommand.test.js +++ /dev/null @@ -1,56 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const CancelOperationCommand = require('../../../../lib/hive/Commands/CancelOperationCommand').default; - -const requestMock = { - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: 0, - modifiedRowCount: 0, - }, -}; - -const responseMock = { - status: { statusCode: 0 }, -}; - -function TCancelOperationReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - CancelOperation(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('CancelOperationCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TCancelOperationReq', TCancelOperationReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new CancelOperationCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/CancelOperationCommand.test.ts b/tests/unit/hive/commands/CancelOperationCommand.test.ts new file mode 100644 index 00000000..c9994905 --- /dev/null +++ b/tests/unit/hive/commands/CancelOperationCommand.test.ts @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import CancelOperationCommand from '../../../../lib/hive/Commands/CancelOperationCommand'; +import { TCancelOperationReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('CancelOperationCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new CancelOperationCommand(thriftClient, new ClientContextStub()); + + const request: TCancelOperationReq = { + operationHandle: { + hasResultSet: true, + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: 0, + modifiedRowCount: 0, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.CancelOperation.called).to.be.true; + expect(thriftClient.cancelOperationReq).to.deep.equal(new TCancelOperationReq(request)); + expect(response).to.be.deep.eq(thriftClient.cancelOperationResp); + }); +}); diff --git a/tests/unit/hive/commands/CloseOperationCommand.test.js b/tests/unit/hive/commands/CloseOperationCommand.test.js deleted file mode 100644 index 79147844..00000000 --- a/tests/unit/hive/commands/CloseOperationCommand.test.js +++ /dev/null @@ -1,56 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const CloseOperationCommand = require('../../../../lib/hive/Commands/CloseOperationCommand').default; - -const requestMock = { - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: 0, - modifiedRowCount: 0, - }, -}; - -const responseMock = { - status: { statusCode: 0 }, -}; - -function TCloseOperationReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - CloseOperation(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('CloseOperationCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TCloseOperationReq', TCloseOperationReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new CloseOperationCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/CloseOperationCommand.test.ts b/tests/unit/hive/commands/CloseOperationCommand.test.ts new file mode 100644 index 00000000..3524624a --- /dev/null +++ b/tests/unit/hive/commands/CloseOperationCommand.test.ts @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import CloseOperationCommand from '../../../../lib/hive/Commands/CloseOperationCommand'; +import { TCloseOperationReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('CloseOperationCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new CloseOperationCommand(thriftClient, new ClientContextStub()); + + const request: TCloseOperationReq = { + operationHandle: { + hasResultSet: true, + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: 0, + modifiedRowCount: 0, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.CloseOperation.called).to.be.true; + expect(thriftClient.closeOperationReq).to.deep.equal(new TCloseOperationReq(request)); + expect(response).to.be.deep.eq(thriftClient.closeOperationResp); + }); +}); diff --git a/tests/unit/hive/commands/CloseSessionCommand.test.js b/tests/unit/hive/commands/CloseSessionCommand.test.js deleted file mode 100644 index 6d15ed56..00000000 --- a/tests/unit/hive/commands/CloseSessionCommand.test.js +++ /dev/null @@ -1,51 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const CloseSessionCommand = require('../../../../lib/hive/Commands/CloseSessionCommand').default; - -const responseMock = { - status: { statusCode: 0 }, -}; - -function TCloseSessionReqMock(options) { - this.options = options; - - expect(options).has.property('sessionHandle'); -} - -const thriftClientMock = { - CloseSession(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('CloseSessionCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TCloseSessionReq', TCloseSessionReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new CloseSessionCommand(thriftClientMock); - - command - .execute({ - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - }) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/CloseSessionCommand.test.ts b/tests/unit/hive/commands/CloseSessionCommand.test.ts new file mode 100644 index 00000000..f2553847 --- /dev/null +++ b/tests/unit/hive/commands/CloseSessionCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import CloseSessionCommand from '../../../../lib/hive/Commands/CloseSessionCommand'; +import { TCloseSessionReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('CloseSessionCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new CloseSessionCommand(thriftClient, new ClientContextStub()); + + const request: TCloseSessionReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.CloseSession.called).to.be.true; + expect(thriftClient.closeSessionReq).to.deep.equal(new TCloseSessionReq(request)); + expect(response).to.be.deep.eq(thriftClient.closeSessionResp); + }); +}); diff --git a/tests/unit/hive/commands/ExecuteStatementCommand.test.js b/tests/unit/hive/commands/ExecuteStatementCommand.test.js deleted file mode 100644 index 8e70337b..00000000 --- a/tests/unit/hive/commands/ExecuteStatementCommand.test.js +++ /dev/null @@ -1,64 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const ExecuteStatementCommand = require('../../../../lib/hive/Commands/ExecuteStatementCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - statement: 'SHOW TABLES', - confOverlay: {}, - queryTimeout: 0, -}; - -const EXECUTE_STATEMENT = 0; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: EXECUTE_STATEMENT, - modifiedRowCount: 0, - }, -}; - -function TExecuteStatementReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - ExecuteStatement(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('ExecuteStatementCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TExecuteStatementReq', TExecuteStatementReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new ExecuteStatementCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/ExecuteStatementCommand.test.ts b/tests/unit/hive/commands/ExecuteStatementCommand.test.ts new file mode 100644 index 00000000..dd49ac66 --- /dev/null +++ b/tests/unit/hive/commands/ExecuteStatementCommand.test.ts @@ -0,0 +1,28 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import Int64 from 'node-int64'; +import ExecuteStatementCommand from '../../../../lib/hive/Commands/ExecuteStatementCommand'; +import { TExecuteStatementReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('ExecuteStatementCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new ExecuteStatementCommand(thriftClient, new ClientContextStub()); + + const request: TExecuteStatementReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + statement: 'SHOW TABLES', + queryTimeout: new Int64(0), + }; + + const response = await command.execute(request); + expect(thriftClient.ExecuteStatement.called).to.be.true; + expect(thriftClient.executeStatementReq).to.deep.equal(new TExecuteStatementReq(request)); + expect(response).to.be.deep.eq(thriftClient.executeStatementResp); + }); +}); diff --git a/tests/unit/hive/commands/FetchResultsCommand.test.js b/tests/unit/hive/commands/FetchResultsCommand.test.js deleted file mode 100644 index 021c0c18..00000000 --- a/tests/unit/hive/commands/FetchResultsCommand.test.js +++ /dev/null @@ -1,75 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const FetchResultsCommand = require('../../../../lib/hive/Commands/FetchResultsCommand').default; - -const requestMock = { - operationHandle: { - sessionId: { guid: '', secret: '' }, - }, - orientation: 0, - maxRows: 100, - fetchType: 0, -}; - -const responseMock = { - status: { statusCode: 0 }, - hasMoreRows: false, - results: { - startRowOffset: 0, - rows: [ - { - colVals: [true, 'value'], - }, - ], - columns: [ - { - values: [true], - }, - { - values: ['value'], - }, - ], - binaryColumns: Buffer.from([]), - columnCount: 2, - }, -}; - -function TFetchResultsReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - FetchResults(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('FetchResultsCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TFetchResultsReq', TFetchResultsReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new FetchResultsCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/FetchResultsCommand.test.ts b/tests/unit/hive/commands/FetchResultsCommand.test.ts new file mode 100644 index 00000000..addeb076 --- /dev/null +++ b/tests/unit/hive/commands/FetchResultsCommand.test.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import Int64 from 'node-int64'; +import FetchResultsCommand from '../../../../lib/hive/Commands/FetchResultsCommand'; +import { TOperationType, TFetchOrientation, TFetchResultsReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('FetchResultsCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new FetchResultsCommand(thriftClient, new ClientContextStub()); + + const request: TFetchResultsReq = { + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: true, + }, + orientation: TFetchOrientation.FETCH_FIRST, + maxRows: new Int64(100), + fetchType: 0, + }; + + const response = await command.execute(request); + expect(thriftClient.FetchResults.called).to.be.true; + expect(thriftClient.fetchResultsReq).to.deep.equal(new TFetchResultsReq(request)); + expect(response).to.be.deep.eq(thriftClient.fetchResultsResp); + }); +}); diff --git a/tests/unit/hive/commands/GetCatalogsCommand.test.js b/tests/unit/hive/commands/GetCatalogsCommand.test.js deleted file mode 100644 index 7c57e661..00000000 --- a/tests/unit/hive/commands/GetCatalogsCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetCatalogsCommand = require('../../../../lib/hive/Commands/GetCatalogsCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_CATALOG = 2; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_CATALOG, - modifiedRowCount: 0, - }, -}; - -function TGetCatalogsReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetCatalogs(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetCatalogsCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetCatalogsReq', TGetCatalogsReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetCatalogsCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetCatalogsCommand.test.ts b/tests/unit/hive/commands/GetCatalogsCommand.test.ts new file mode 100644 index 00000000..99d6c17e --- /dev/null +++ b/tests/unit/hive/commands/GetCatalogsCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetCatalogsCommand from '../../../../lib/hive/Commands/GetCatalogsCommand'; +import { TGetCatalogsReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetCatalogsCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetCatalogsCommand(thriftClient, new ClientContextStub()); + + const request: TGetCatalogsReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetCatalogs.called).to.be.true; + expect(thriftClient.getCatalogsReq).to.deep.equal(new TGetCatalogsReq(request)); + expect(response).to.be.deep.eq(thriftClient.getCatalogsResp); + }); +}); diff --git a/tests/unit/hive/commands/GetColumnsCommand.test.js b/tests/unit/hive/commands/GetColumnsCommand.test.js deleted file mode 100644 index 062f420b..00000000 --- a/tests/unit/hive/commands/GetColumnsCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetColumnsCommand = require('../../../../lib/hive/Commands/GetColumnsCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_COLUMNS = 6; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_COLUMNS, - modifiedRowCount: 0, - }, -}; - -function TGetColumnsReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetColumns(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetColumnsCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetColumnsReq', TGetColumnsReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetColumnsCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetColumnsCommand.test.ts b/tests/unit/hive/commands/GetColumnsCommand.test.ts new file mode 100644 index 00000000..cc22ec95 --- /dev/null +++ b/tests/unit/hive/commands/GetColumnsCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetColumnsCommand from '../../../../lib/hive/Commands/GetColumnsCommand'; +import { TGetColumnsReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetColumnsCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetColumnsCommand(thriftClient, new ClientContextStub()); + + const request: TGetColumnsReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetColumns.called).to.be.true; + expect(thriftClient.getColumnsReq).to.deep.equal(new TGetColumnsReq(request)); + expect(response).to.be.deep.eq(thriftClient.getColumnsResp); + }); +}); diff --git a/tests/unit/hive/commands/GetCrossReferenceCommand.test.js b/tests/unit/hive/commands/GetCrossReferenceCommand.test.js deleted file mode 100644 index 99ca435e..00000000 --- a/tests/unit/hive/commands/GetCrossReferenceCommand.test.js +++ /dev/null @@ -1,67 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetCrossReferenceCommand = require('../../../../lib/hive/Commands/GetCrossReferenceCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - parentCatalogName: 'parentCatalogName', - parentSchemaName: 'parentSchemaName', - parentTableName: 'parentTableName', - foreignCatalogName: 'foreignCatalogName', - foreignSchemaName: 'foreignSchemaName', - foreignTableName: 'foreignTableName', -}; - -const GET_CROSS_REFERENCE = 7; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_CROSS_REFERENCE, - modifiedRowCount: 0, - }, -}; - -function TGetCrossReferenceReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetCrossReference(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetCrossReferenceCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetCrossReferenceReq', TGetCrossReferenceReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetCrossReferenceCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetCrossReferenceCommand.test.ts b/tests/unit/hive/commands/GetCrossReferenceCommand.test.ts new file mode 100644 index 00000000..b3c6dfb6 --- /dev/null +++ b/tests/unit/hive/commands/GetCrossReferenceCommand.test.ts @@ -0,0 +1,31 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetCrossReferenceCommand from '../../../../lib/hive/Commands/GetCrossReferenceCommand'; +import { TGetCrossReferenceReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetCrossReferenceCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetCrossReferenceCommand(thriftClient, new ClientContextStub()); + + const request: TGetCrossReferenceReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + parentCatalogName: 'parentCatalogName', + parentSchemaName: 'parentSchemaName', + parentTableName: 'parentTableName', + foreignCatalogName: 'foreignCatalogName', + foreignSchemaName: 'foreignSchemaName', + foreignTableName: 'foreignTableName', + }; + + const response = await command.execute(request); + expect(thriftClient.GetCrossReference.called).to.be.true; + expect(thriftClient.getCrossReferenceReq).to.deep.equal(new TGetCrossReferenceReq(request)); + expect(response).to.be.deep.eq(thriftClient.getCrossReferenceResp); + }); +}); diff --git a/tests/unit/hive/commands/GetDelegationTokenCommand.test.js b/tests/unit/hive/commands/GetDelegationTokenCommand.test.js deleted file mode 100644 index 9f715f5d..00000000 --- a/tests/unit/hive/commands/GetDelegationTokenCommand.test.js +++ /dev/null @@ -1,56 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetDelegationTokenCommand = require('../../../../lib/hive/Commands/GetDelegationTokenCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - owner: 'user1', - renewer: 'user2', -}; - -const responseMock = { - status: { statusCode: 0 }, - delegationToken: 'token', -}; - -function TGetDelegationTokenReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetDelegationToken(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetDelegationTokenCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetDelegationTokenReq', TGetDelegationTokenReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetDelegationTokenCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetDelegationTokenCommand.test.ts b/tests/unit/hive/commands/GetDelegationTokenCommand.test.ts new file mode 100644 index 00000000..61c796e5 --- /dev/null +++ b/tests/unit/hive/commands/GetDelegationTokenCommand.test.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetDelegationTokenCommand from '../../../../lib/hive/Commands/GetDelegationTokenCommand'; +import { TGetDelegationTokenReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetDelegationTokenCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetDelegationTokenCommand(thriftClient, new ClientContextStub()); + + const request: TGetDelegationTokenReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + owner: 'user1', + renewer: 'user2', + }; + + const response = await command.execute(request); + expect(thriftClient.GetDelegationToken.called).to.be.true; + expect(thriftClient.getDelegationTokenReq).to.deep.equal(new TGetDelegationTokenReq(request)); + expect(response).to.be.deep.eq(thriftClient.getDelegationTokenResp); + }); +}); diff --git a/tests/unit/hive/commands/GetFunctionsCommand.test.js b/tests/unit/hive/commands/GetFunctionsCommand.test.js deleted file mode 100644 index 07a37a58..00000000 --- a/tests/unit/hive/commands/GetFunctionsCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetFunctionsCommand = require('../../../../lib/hive/Commands/GetFunctionsCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_FUNCTIONS = 7; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_FUNCTIONS, - modifiedRowCount: 0, - }, -}; - -function TGetFunctionsReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetFunctions(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetFunctionsCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetFunctionsReq', TGetFunctionsReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetFunctionsCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetFunctionsCommand.test.ts b/tests/unit/hive/commands/GetFunctionsCommand.test.ts new file mode 100644 index 00000000..9973e539 --- /dev/null +++ b/tests/unit/hive/commands/GetFunctionsCommand.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetFunctionsCommand from '../../../../lib/hive/Commands/GetFunctionsCommand'; +import { TGetFunctionsReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetFunctionsCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetFunctionsCommand(thriftClient, new ClientContextStub()); + + const request: TGetFunctionsReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + functionName: 'test', + }; + + const response = await command.execute(request); + expect(thriftClient.GetFunctions.called).to.be.true; + expect(thriftClient.getFunctionsReq).to.deep.equal(new TGetFunctionsReq(request)); + expect(response).to.be.deep.eq(thriftClient.getFunctionsResp); + }); +}); diff --git a/tests/unit/hive/commands/GetInfoCommand.test.js b/tests/unit/hive/commands/GetInfoCommand.test.js deleted file mode 100644 index 1ceaa711..00000000 --- a/tests/unit/hive/commands/GetInfoCommand.test.js +++ /dev/null @@ -1,62 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetInfoCommand = require('../../../../lib/hive/Commands/GetInfoCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - infoType: 0, -}; - -const responseMock = { - status: { statusCode: 0 }, - infoValue: { - stringValue: '', - smallIntValue: 0, - integerBitmask: 1, - integerFlag: 0, - binaryValue: Buffer.from([]), - lenValue: Buffer.from([]), - }, -}; - -function TGetInfoReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetInfo(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetInfoCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetInfoReq', TGetInfoReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetInfoCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetInfoCommand.test.ts b/tests/unit/hive/commands/GetInfoCommand.test.ts new file mode 100644 index 00000000..9cb7bbe8 --- /dev/null +++ b/tests/unit/hive/commands/GetInfoCommand.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetInfoCommand from '../../../../lib/hive/Commands/GetInfoCommand'; +import { TGetInfoReq, TGetInfoType } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetInfoCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetInfoCommand(thriftClient, new ClientContextStub()); + + const request: TGetInfoReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + infoType: TGetInfoType.CLI_SERVER_NAME, + }; + + const response = await command.execute(request); + expect(thriftClient.GetInfo.called).to.be.true; + expect(thriftClient.getInfoReq).to.deep.equal(new TGetInfoReq(request)); + expect(response).to.be.deep.eq(thriftClient.getInfoResp); + }); +}); diff --git a/tests/unit/hive/commands/GetOperationStatusCommand.test.js b/tests/unit/hive/commands/GetOperationStatusCommand.test.js deleted file mode 100644 index d84aae05..00000000 --- a/tests/unit/hive/commands/GetOperationStatusCommand.test.js +++ /dev/null @@ -1,74 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetOperationStatusCommand = require('../../../../lib/hive/Commands/GetOperationStatusCommand').default; - -const requestMock = { - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: 0, - modifiedRowCount: 0, - }, - getProgressUpdate: true, -}; - -const responseMock = { - status: { statusCode: 0 }, - operationState: 2, - sqlState: '', - errorCode: 0, - errorMessage: '', - taskStatus: '', - operationStarted: Buffer.from([]), - operationCompleted: Buffer.from([]), - hasResultSet: true, - progressUpdateResponse: { - headerNames: [''], - rows: [['']], - progressedPercentage: 50, - status: 0, - footerSummary: '', - startTime: Buffer.from([]), - }, - numModifiedRows: Buffer.from([]), -}; - -function TGetOperationStatusReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetOperationStatus(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetOperationStatusCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetOperationStatusReq', TGetOperationStatusReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetOperationStatusCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetOperationStatusCommand.test.ts b/tests/unit/hive/commands/GetOperationStatusCommand.test.ts new file mode 100644 index 00000000..1edf0569 --- /dev/null +++ b/tests/unit/hive/commands/GetOperationStatusCommand.test.ts @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetOperationStatusCommand from '../../../../lib/hive/Commands/GetOperationStatusCommand'; +import { TGetOperationStatusReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetOperationStatusCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetOperationStatusCommand(thriftClient, new ClientContextStub()); + + const request: TGetOperationStatusReq = { + operationHandle: { + hasResultSet: true, + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: 0, + modifiedRowCount: 0, + }, + getProgressUpdate: true, + }; + + const response = await command.execute(request); + expect(thriftClient.GetOperationStatus.called).to.be.true; + expect(thriftClient.getOperationStatusReq).to.deep.equal(new TGetOperationStatusReq(request)); + expect(response).to.be.deep.eq(thriftClient.getOperationStatusResp); + }); +}); diff --git a/tests/unit/hive/commands/GetPrimaryKeysCommand.test.js b/tests/unit/hive/commands/GetPrimaryKeysCommand.test.js deleted file mode 100644 index f3044454..00000000 --- a/tests/unit/hive/commands/GetPrimaryKeysCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetPrimaryKeysCommand = require('../../../../lib/hive/Commands/GetPrimaryKeysCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_PRIMARY_KEYS = 7; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_PRIMARY_KEYS, - modifiedRowCount: 0, - }, -}; - -function TGetPrimaryKeysReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetPrimaryKeys(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetPrimaryKeysCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetPrimaryKeysReq', TGetPrimaryKeysReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetPrimaryKeysCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetPrimaryKeysCommand.test.ts b/tests/unit/hive/commands/GetPrimaryKeysCommand.test.ts new file mode 100644 index 00000000..66db778d --- /dev/null +++ b/tests/unit/hive/commands/GetPrimaryKeysCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetPrimaryKeysCommand from '../../../../lib/hive/Commands/GetPrimaryKeysCommand'; +import { TGetPrimaryKeysReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetPrimaryKeysCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetPrimaryKeysCommand(thriftClient, new ClientContextStub()); + + const request: TGetPrimaryKeysReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetPrimaryKeys.called).to.be.true; + expect(thriftClient.getPrimaryKeysReq).to.deep.equal(new TGetPrimaryKeysReq(request)); + expect(response).to.be.deep.eq(thriftClient.getPrimaryKeysResp); + }); +}); diff --git a/tests/unit/hive/commands/GetResultSetMetadataCommand.test.js b/tests/unit/hive/commands/GetResultSetMetadataCommand.test.js deleted file mode 100644 index b426acc7..00000000 --- a/tests/unit/hive/commands/GetResultSetMetadataCommand.test.js +++ /dev/null @@ -1,69 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetResultSetMetadataCommand = require('../../../../lib/hive/Commands/GetResultSetMetadataCommand').default; - -const requestMock = { - operationHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const responseMock = { - status: { statusCode: 0 }, - schema: { - columns: [ - { - columnName: 'column1', - typeDesc: { - types: [ - { - type: 0, - }, - ], - }, - position: 0, - comment: '', - }, - ], - }, -}; - -function TGetResultSetMetadataReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetResultSetMetadata(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetResultSetMetadataCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetResultSetMetadataReq', TGetResultSetMetadataReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetResultSetMetadataCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetResultSetMetadataCommand.test.ts b/tests/unit/hive/commands/GetResultSetMetadataCommand.test.ts new file mode 100644 index 00000000..a7c7cb7c --- /dev/null +++ b/tests/unit/hive/commands/GetResultSetMetadataCommand.test.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetResultSetMetadataCommand from '../../../../lib/hive/Commands/GetResultSetMetadataCommand'; +import { TOperationType, TGetResultSetMetadataReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetResultSetMetadataCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetResultSetMetadataCommand(thriftClient, new ClientContextStub()); + + const request: TGetResultSetMetadataReq = { + operationHandle: { + operationId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + operationType: TOperationType.EXECUTE_STATEMENT, + hasResultSet: true, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetResultSetMetadata.called).to.be.true; + expect(thriftClient.getResultSetMetadataReq).to.deep.equal(new TGetResultSetMetadataReq(request)); + expect(response).to.be.deep.eq(thriftClient.getResultSetMetadataResp); + }); +}); diff --git a/tests/unit/hive/commands/GetSchemasCommand.test.js b/tests/unit/hive/commands/GetSchemasCommand.test.js deleted file mode 100644 index 5fc5122e..00000000 --- a/tests/unit/hive/commands/GetSchemasCommand.test.js +++ /dev/null @@ -1,63 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetSchemasCommand = require('../../../../lib/hive/Commands/GetSchemasCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - catalogName: 'catalog', - schemaName: 'schema', -}; - -const GET_SCHEMAS = 3; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_SCHEMAS, - modifiedRowCount: 0, - }, -}; - -function TGetSchemasReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetSchemas(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetSchemasCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetSchemasReq', TGetSchemasReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetSchemasCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetSchemasCommand.test.ts b/tests/unit/hive/commands/GetSchemasCommand.test.ts new file mode 100644 index 00000000..b14461d0 --- /dev/null +++ b/tests/unit/hive/commands/GetSchemasCommand.test.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetSchemasCommand from '../../../../lib/hive/Commands/GetSchemasCommand'; +import { TGetSchemasReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetSchemasCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetSchemasCommand(thriftClient, new ClientContextStub()); + + const request: TGetSchemasReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + catalogName: 'catalog', + schemaName: 'schema', + }; + + const response = await command.execute(request); + expect(thriftClient.GetSchemas.called).to.be.true; + expect(thriftClient.getSchemasReq).to.deep.equal(new TGetSchemasReq(request)); + expect(response).to.be.deep.eq(thriftClient.getSchemasResp); + }); +}); diff --git a/tests/unit/hive/commands/GetTableTypesCommand.test.js b/tests/unit/hive/commands/GetTableTypesCommand.test.js deleted file mode 100644 index 02601515..00000000 --- a/tests/unit/hive/commands/GetTableTypesCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetTableTypesCommand = require('../../../../lib/hive/Commands/GetTableTypesCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_TABLE_TYPES = 5; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_TABLE_TYPES, - modifiedRowCount: 0, - }, -}; - -function TGetTableTypesReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetTableTypes(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetTableTypesCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetTableTypesReq', TGetTableTypesReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetTableTypesCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetTableTypesCommand.test.ts b/tests/unit/hive/commands/GetTableTypesCommand.test.ts new file mode 100644 index 00000000..9c82a627 --- /dev/null +++ b/tests/unit/hive/commands/GetTableTypesCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetTableTypesCommand from '../../../../lib/hive/Commands/GetTableTypesCommand'; +import { TGetTableTypesReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetTableTypesCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetTableTypesCommand(thriftClient, new ClientContextStub()); + + const request: TGetTableTypesReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetTableTypes.called).to.be.true; + expect(thriftClient.getTableTypesReq).to.deep.equal(new TGetTableTypesReq(request)); + expect(response).to.be.deep.eq(thriftClient.getTableTypesResp); + }); +}); diff --git a/tests/unit/hive/commands/GetTablesCommand.test.js b/tests/unit/hive/commands/GetTablesCommand.test.js deleted file mode 100644 index 994c5030..00000000 --- a/tests/unit/hive/commands/GetTablesCommand.test.js +++ /dev/null @@ -1,65 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetTablesCommand = require('../../../../lib/hive/Commands/GetTablesCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - catalogName: 'catalog', - schemaName: 'schema', - tableName: 'table', - tableTypes: ['TABLE', 'VIEW', 'SYSTEM TABLE', 'GLOBAL TEMPORARY', 'LOCAL TEMPORARY', 'ALIAS', 'SYNONYM'], -}; - -const GET_TABLES = 4; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_TABLES, - modifiedRowCount: 0, - }, -}; - -function TGetTablesReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetTables(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetTablesCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetTablesReq', TGetTablesReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetTablesCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetTablesCommand.test.ts b/tests/unit/hive/commands/GetTablesCommand.test.ts new file mode 100644 index 00000000..bc14e930 --- /dev/null +++ b/tests/unit/hive/commands/GetTablesCommand.test.ts @@ -0,0 +1,29 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetTablesCommand from '../../../../lib/hive/Commands/GetTablesCommand'; +import { TGetTablesReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetTablesCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetTablesCommand(thriftClient, new ClientContextStub()); + + const request: TGetTablesReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + catalogName: 'catalog', + schemaName: 'schema', + tableName: 'table', + tableTypes: ['TABLE', 'VIEW', 'SYSTEM TABLE', 'GLOBAL TEMPORARY', 'LOCAL TEMPORARY', 'ALIAS', 'SYNONYM'], + }; + + const response = await command.execute(request); + expect(thriftClient.GetTables.called).to.be.true; + expect(thriftClient.getTablesReq).to.deep.equal(new TGetTablesReq(request)); + expect(response).to.be.deep.eq(thriftClient.getTablesResp); + }); +}); diff --git a/tests/unit/hive/commands/GetTypeInfoCommand.test.js b/tests/unit/hive/commands/GetTypeInfoCommand.test.js deleted file mode 100644 index 0bd9dbdc..00000000 --- a/tests/unit/hive/commands/GetTypeInfoCommand.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const GetTypeInfoCommand = require('../../../../lib/hive/Commands/GetTypeInfoCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, -}; - -const GET_TYPE_INFO = 1; - -const responseMock = { - status: { statusCode: 0 }, - operationHandle: { - hasResultSet: true, - operationId: { guid: '', secret: '' }, - operationType: GET_TYPE_INFO, - modifiedRowCount: 0, - }, -}; - -function TGetTypeInfoReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - GetTypeInfo(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('GetTypeInfoCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TGetTypeInfoReq', TGetTypeInfoReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new GetTypeInfoCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/GetTypeInfoCommand.test.ts b/tests/unit/hive/commands/GetTypeInfoCommand.test.ts new file mode 100644 index 00000000..b5610ef0 --- /dev/null +++ b/tests/unit/hive/commands/GetTypeInfoCommand.test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import GetTypeInfoCommand from '../../../../lib/hive/Commands/GetTypeInfoCommand'; +import { TGetTypeInfoReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('GetTypeInfoCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new GetTypeInfoCommand(thriftClient, new ClientContextStub()); + + const request: TGetTypeInfoReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + }; + + const response = await command.execute(request); + expect(thriftClient.GetTypeInfo.called).to.be.true; + expect(thriftClient.getTypeInfoReq).to.deep.equal(new TGetTypeInfoReq(request)); + expect(response).to.be.deep.eq(thriftClient.getTypeInfoResp); + }); +}); diff --git a/tests/unit/hive/commands/OpenSessionCommand.test.js b/tests/unit/hive/commands/OpenSessionCommand.test.js deleted file mode 100644 index af3a5800..00000000 --- a/tests/unit/hive/commands/OpenSessionCommand.test.js +++ /dev/null @@ -1,56 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const OpenSessionCommand = require('../../../../lib/hive/Commands/OpenSessionCommand').default; - -const CLIENT_PROTOCOL = 8; - -const responseMock = { - status: { statusCode: 0 }, - serverProtocolVersion: CLIENT_PROTOCOL, - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - configuration: {}, -}; - -function TOpenSessionReqMock(options) { - this.options = options; - - expect(options.client_protocol).to.be.eq(CLIENT_PROTOCOL); -} - -const thriftClientMock = { - OpenSession(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('OpenSessionCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TOpenSessionReq', TOpenSessionReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new OpenSessionCommand(thriftClientMock); - - command - .execute({ - client_protocol: CLIENT_PROTOCOL, - }) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/OpenSessionCommand.test.ts b/tests/unit/hive/commands/OpenSessionCommand.test.ts new file mode 100644 index 00000000..78a3a076 --- /dev/null +++ b/tests/unit/hive/commands/OpenSessionCommand.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import OpenSessionCommand from '../../../../lib/hive/Commands/OpenSessionCommand'; +import { TProtocolVersion, TOpenSessionReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('OpenSessionCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new OpenSessionCommand(thriftClient, new ClientContextStub()); + + const request: TOpenSessionReq = { + client_protocol: TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V8, + }; + + const response = await command.execute(request); + expect(thriftClient.OpenSession.called).to.be.true; + expect(thriftClient.openSessionReq).to.deep.equal(new TOpenSessionReq(request)); + expect(response).to.be.deep.eq(thriftClient.openSessionResp); + }); +}); diff --git a/tests/unit/hive/commands/RenewDelegationTokenCommand.test.js b/tests/unit/hive/commands/RenewDelegationTokenCommand.test.js deleted file mode 100644 index 12b44e0a..00000000 --- a/tests/unit/hive/commands/RenewDelegationTokenCommand.test.js +++ /dev/null @@ -1,54 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const TCLIService_types = require('../../../../thrift/TCLIService_types'); -const RenewDelegationTokenCommand = require('../../../../lib/hive/Commands/RenewDelegationTokenCommand').default; - -const requestMock = { - sessionHandle: { - sessionId: { guid: '', secret: '' }, - }, - delegationToken: 'token', -}; - -const responseMock = { - status: { statusCode: 0 }, -}; - -function TRenewDelegationTokenReqMock(options) { - this.options = options; - - expect(options).to.be.deep.eq(requestMock); -} - -const thriftClientMock = { - RenewDelegationToken(request, callback) { - return callback(null, responseMock); - }, -}; - -describe('RenewDelegationTokenCommand', () => { - let sandbox; - - before(() => { - sandbox = sinon.createSandbox(); - sandbox.replace(TCLIService_types, 'TRenewDelegationTokenReq', TRenewDelegationTokenReqMock); - }); - - after(() => { - sandbox.restore(); - }); - - it('should return response', (cb) => { - const command = new RenewDelegationTokenCommand(thriftClientMock); - - command - .execute(requestMock) - .then((response) => { - expect(response).to.be.deep.eq(responseMock); - cb(); - }) - .catch((error) => { - cb(error); - }); - }); -}); diff --git a/tests/unit/hive/commands/RenewDelegationTokenCommand.test.ts b/tests/unit/hive/commands/RenewDelegationTokenCommand.test.ts new file mode 100644 index 00000000..a0072027 --- /dev/null +++ b/tests/unit/hive/commands/RenewDelegationTokenCommand.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import RenewDelegationTokenCommand from '../../../../lib/hive/Commands/RenewDelegationTokenCommand'; +import { TRenewDelegationTokenReq } from '../../../../thrift/TCLIService_types'; + +import ClientContextStub from '../../.stubs/ClientContextStub'; +import ThriftClientStub from '../../.stubs/ThriftClientStub'; + +describe('RenewDelegationTokenCommand', () => { + it('should return response', async () => { + const thriftClient = sinon.spy(new ThriftClientStub()); + const command = new RenewDelegationTokenCommand(thriftClient, new ClientContextStub()); + + const request: TRenewDelegationTokenReq = { + sessionHandle: { + sessionId: { guid: Buffer.alloc(16), secret: Buffer.alloc(16) }, + }, + delegationToken: 'token', + }; + + const response = await command.execute(request); + expect(thriftClient.RenewDelegationToken.called).to.be.true; + expect(thriftClient.renewDelegationTokenReq).to.deep.equal(new TRenewDelegationTokenReq(request)); + expect(response).to.be.deep.eq(thriftClient.renewDelegationTokenResp); + }); +}); diff --git a/tests/unit/polyfills.test.js b/tests/unit/polyfills.test.ts similarity index 70% rename from tests/unit/polyfills.test.js rename to tests/unit/polyfills.test.ts index 80571966..c5658339 100644 --- a/tests/unit/polyfills.test.js +++ b/tests/unit/polyfills.test.ts @@ -1,7 +1,7 @@ -const { expect } = require('chai'); -const { at } = require('../../lib/polyfills'); +import { expect } from 'chai'; +import { at } from '../../lib/polyfills'; -const defaultArrayMock = { +const arrayLikeStub = { 0: 'a', 1: 'b', 2: 'c', @@ -12,65 +12,65 @@ const defaultArrayMock = { describe('Array.at', () => { it('should handle zero index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(0)).to.eq('a'); expect(obj.at(Number('+0'))).to.eq('a'); expect(obj.at(Number('-0'))).to.eq('a'); }); it('should handle positive index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(2)).to.eq('c'); expect(obj.at(2.2)).to.eq('c'); expect(obj.at(2.8)).to.eq('c'); }); it('should handle negative index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(-2)).to.eq('c'); expect(obj.at(-2.2)).to.eq('c'); expect(obj.at(-2.8)).to.eq('c'); }); it('should handle positive infinity index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(Number.POSITIVE_INFINITY)).to.be.undefined; }); it('should handle negative infinity index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(Number.NEGATIVE_INFINITY)).to.be.undefined; }); it('should handle non-numeric index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at('2')).to.eq('c'); }); it('should handle NaN index', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(Number.NaN)).to.eq('a'); expect(obj.at('invalid')).to.eq('a'); }); it('should handle index out of bounds', () => { - const obj = { ...defaultArrayMock }; + const obj = { ...arrayLikeStub }; expect(obj.at(10)).to.be.undefined; expect(obj.at(-10)).to.be.undefined; }); it('should handle zero length', () => { - const obj = { ...defaultArrayMock, length: 0 }; + const obj = { ...arrayLikeStub, length: 0 }; expect(obj.at(2)).to.be.undefined; }); it('should handle negative length', () => { - const obj = { ...defaultArrayMock, length: -4 }; + const obj = { ...arrayLikeStub, length: -4 }; expect(obj.at(2)).to.be.undefined; }); it('should handle non-numeric length', () => { - const obj = { ...defaultArrayMock, length: 'invalid' }; + const obj = { ...arrayLikeStub, length: 'invalid' as any }; expect(obj.at(2)).to.be.undefined; }); }); diff --git a/tests/unit/result/fixtures/arrowSchemaAllNulls.arrow b/tests/unit/result/.stubs/arrowSchemaAllNulls.arrow similarity index 100% rename from tests/unit/result/fixtures/arrowSchemaAllNulls.arrow rename to tests/unit/result/.stubs/arrowSchemaAllNulls.arrow diff --git a/tests/unit/result/fixtures/dataAllNulls.arrow b/tests/unit/result/.stubs/dataAllNulls.arrow similarity index 100% rename from tests/unit/result/fixtures/dataAllNulls.arrow rename to tests/unit/result/.stubs/dataAllNulls.arrow diff --git a/tests/unit/result/.stubs/thriftSchemaAllNulls.ts b/tests/unit/result/.stubs/thriftSchemaAllNulls.ts new file mode 100644 index 00000000..c95785af --- /dev/null +++ b/tests/unit/result/.stubs/thriftSchemaAllNulls.ts @@ -0,0 +1,232 @@ +import { TTableSchema } from '../../../../thrift/TCLIService_types'; + +const thriftSchema: TTableSchema = { + columns: [ + { + columnName: 'boolean_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 0 }, + }, + ], + }, + position: 1, + comment: '', + }, + { + columnName: 'tinyint_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 1 }, + }, + ], + }, + position: 2, + comment: '', + }, + { + columnName: 'smallint_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 2 }, + }, + ], + }, + position: 3, + comment: '', + }, + { + columnName: 'int_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 3 }, + }, + ], + }, + position: 4, + comment: '', + }, + { + columnName: 'bigint_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 4 }, + }, + ], + }, + position: 5, + comment: '', + }, + { + columnName: 'float_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 5 }, + }, + ], + }, + position: 6, + comment: '', + }, + { + columnName: 'double_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 6 }, + }, + ], + }, + position: 7, + comment: '', + }, + { + columnName: 'decimal_field', + typeDesc: { + types: [ + { + primitiveEntry: { + type: 15, + typeQualifiers: { + qualifiers: { + scale: { i32Value: 2 }, + precision: { i32Value: 6 }, + }, + }, + }, + }, + ], + }, + position: 8, + comment: '', + }, + { + columnName: 'string_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 7 }, + }, + ], + }, + position: 9, + comment: '', + }, + { + columnName: 'char_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 7 }, + }, + ], + }, + position: 10, + comment: '', + }, + { + columnName: 'varchar_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 7 }, + }, + ], + }, + position: 11, + comment: '', + }, + { + columnName: 'timestamp_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 8 }, + }, + ], + }, + position: 12, + comment: '', + }, + { + columnName: 'date_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 17 }, + }, + ], + }, + position: 13, + comment: '', + }, + { + columnName: 'day_interval_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 7 }, + }, + ], + }, + position: 14, + comment: '', + }, + { + columnName: 'month_interval_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 7 }, + }, + ], + }, + position: 15, + comment: '', + }, + { + columnName: 'binary_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 9 }, + }, + ], + }, + position: 16, + comment: '', + }, + { + columnName: 'struct_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 12 }, + }, + ], + }, + position: 17, + comment: '', + }, + { + columnName: 'array_field', + typeDesc: { + types: [ + { + primitiveEntry: { type: 10 }, + }, + ], + }, + position: 18, + comment: '', + }, + ], +}; + +export default thriftSchema; diff --git a/tests/unit/result/ArrowResultConverter.test.js b/tests/unit/result/ArrowResultConverter.test.ts similarity index 68% rename from tests/unit/result/ArrowResultConverter.test.js rename to tests/unit/result/ArrowResultConverter.test.ts index 8ac2e1dd..5f940544 100644 --- a/tests/unit/result/ArrowResultConverter.test.js +++ b/tests/unit/result/ArrowResultConverter.test.ts @@ -1,11 +1,17 @@ -const { expect } = require('chai'); -const fs = require('fs'); -const path = require('path'); -const { tableFromArrays, tableToIPC, Table } = require('apache-arrow'); -const ArrowResultConverter = require('../../../lib/result/ArrowResultConverter').default; -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); - -function createSampleThriftSchema(columnName) { +import { expect } from 'chai'; +import fs from 'fs'; +import path from 'path'; +import { Table, tableFromArrays, tableToIPC, RecordBatch, TypeMap } from 'apache-arrow'; +import ArrowResultConverter from '../../../lib/result/ArrowResultConverter'; +import { ArrowBatch } from '../../../lib/result/utils'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; +import { TStatusCode, TTableSchema, TTypeId } from '../../../thrift/TCLIService_types'; + +import ClientContextStub from '../.stubs/ClientContextStub'; + +import thriftSchemaAllNulls from './.stubs/thriftSchemaAllNulls'; + +function createSampleThriftSchema(columnName: string): TTableSchema { return { columns: [ { @@ -14,8 +20,7 @@ function createSampleThriftSchema(columnName) { types: [ { primitiveEntry: { - type: 3, - typeQualifiers: null, + type: TTypeId.INT_TYPE, }, }, ], @@ -49,36 +54,31 @@ const sampleArrowBatch = [ ]), ]; -const thriftSchemaAllNulls = JSON.parse( - fs.readFileSync(path.join(__dirname, 'fixtures/thriftSchemaAllNulls.json')).toString('utf-8'), -); - const arrowBatchAllNulls = [ - fs.readFileSync(path.join(__dirname, 'fixtures/arrowSchemaAllNulls.arrow')), - fs.readFileSync(path.join(__dirname, 'fixtures/dataAllNulls.arrow')), + fs.readFileSync(path.join(__dirname, './.stubs/arrowSchemaAllNulls.arrow')), + fs.readFileSync(path.join(__dirname, './.stubs/dataAllNulls.arrow')), ]; -const emptyItem = { +const emptyItem: ArrowBatch = { batches: [], rowCount: 0, }; -function createSampleRecordBatch(start, count) { +function createSampleRecordBatch(start: number, count: number) { const table = tableFromArrays({ id: Float64Array.from({ length: count }, (unused, index) => index + start), }); return table.batches[0]; } -function createSampleArrowBatch(...recordBatches) { +function createSampleArrowBatch(...recordBatches: RecordBatch[]) { const table = new Table(recordBatches); - return tableToIPC(table); + return Buffer.from(tableToIPC(table)); } describe('ArrowResultConverter', () => { it('should convert data', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock( + const rowSetProvider = new ResultsProviderStub( [ { batches: sampleArrowBatch, @@ -87,21 +87,25 @@ describe('ArrowResultConverter', () => { ], emptyItem, ); - const result = new ArrowResultConverter(context, rowSetProvider, { schema: sampleThriftSchema }); + const result = new ArrowResultConverter(new ClientContextStub(), rowSetProvider, { + schema: sampleThriftSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([{ 1: 1 }]); }); it('should return empty array if no data to process', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([], emptyItem); - const result = new ArrowResultConverter(context, rowSetProvider, { schema: sampleThriftSchema }); + const rowSetProvider = new ResultsProviderStub([], emptyItem); + const result = new ArrowResultConverter(new ClientContextStub(), rowSetProvider, { + schema: sampleThriftSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([]); expect(await result.hasMore()).to.be.false; }); it('should return empty array if no schema available', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock( + const rowSetProvider = new ResultsProviderStub( [ { batches: sampleArrowBatch, @@ -110,14 +114,16 @@ describe('ArrowResultConverter', () => { ], emptyItem, ); - const result = new ArrowResultConverter(context, rowSetProvider, {}); + const result = new ArrowResultConverter(new ClientContextStub(), rowSetProvider, { + schema: undefined, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.hasMore()).to.be.false; expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([]); }); it('should detect nulls', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock( + const rowSetProvider = new ResultsProviderStub( [ { batches: arrowBatchAllNulls, @@ -126,7 +132,10 @@ describe('ArrowResultConverter', () => { ], emptyItem, ); - const result = new ArrowResultConverter(context, rowSetProvider, { schema: thriftSchemaAllNulls }); + const result = new ArrowResultConverter(new ClientContextStub(), rowSetProvider, { + schema: thriftSchemaAllNulls, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([ { boolean_field: null, @@ -158,9 +167,7 @@ describe('ArrowResultConverter', () => { }); it('should respect row count in batch', async () => { - const context = {}; - - const rowSetProvider = new ResultsProviderMock( + const rowSetProvider = new ResultsProviderStub( [ // First Arrow batch: contains two record batches of 5 and 5 record, // but declared count of rows is 8. It means that result should @@ -180,7 +187,10 @@ describe('ArrowResultConverter', () => { ], emptyItem, ); - const result = new ArrowResultConverter(context, rowSetProvider, { schema: createSampleThriftSchema('id') }); + const result = new ArrowResultConverter(new ClientContextStub(), rowSetProvider, { + schema: createSampleThriftSchema('id'), + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); const rows1 = await result.fetchNext({ limit: 10000 }); expect(rows1).to.deep.equal([{ id: 10 }, { id: 11 }, { id: 12 }, { id: 13 }, { id: 14 }]); diff --git a/tests/unit/result/ArrowResultHandler.test.js b/tests/unit/result/ArrowResultHandler.test.ts similarity index 51% rename from tests/unit/result/ArrowResultHandler.test.js rename to tests/unit/result/ArrowResultHandler.test.ts index b6852deb..c657b16b 100644 --- a/tests/unit/result/ArrowResultHandler.test.js +++ b/tests/unit/result/ArrowResultHandler.test.ts @@ -1,8 +1,11 @@ -const { expect } = require('chai'); -const Int64 = require('node-int64'); -const LZ4 = require('lz4'); -const ArrowResultHandler = require('../../../lib/result/ArrowResultHandler').default; -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); +import { expect } from 'chai'; +import Int64 from 'node-int64'; +import LZ4 from 'lz4'; +import ArrowResultHandler from '../../../lib/result/ArrowResultHandler'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; +import { TRowSet, TSparkArrowBatch, TStatusCode, TTableSchema } from '../../../thrift/TCLIService_types'; + +import ClientContextStub from '../.stubs/ClientContextStub'; const sampleArrowSchema = Buffer.from([ 255, 255, 255, 255, 208, 0, 0, 0, 16, 0, 0, 0, 0, 0, 10, 0, 14, 0, 6, 0, 13, 0, 8, 0, 10, 0, 0, 0, 0, 0, 4, 0, 16, 0, @@ -14,7 +17,7 @@ const sampleArrowSchema = Buffer.from([ 0, 0, 0, 0, ]); -const sampleArrowBatch = { +const sampleArrowBatch: TSparkArrowBatch = { batch: Buffer.from([ 255, 255, 255, 255, 136, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 12, 0, 22, 0, 14, 0, 21, 0, 16, 0, 4, 0, 12, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 16, 0, 0, 0, 0, 3, 10, 0, 24, 0, 12, 0, 8, 0, 4, 0, 10, 0, 0, 0, 20, 0, 0, 0, 56, @@ -25,34 +28,39 @@ const sampleArrowBatch = { rowCount: new Int64(1), }; -const sampleRowSet1 = { +const sampleRowSet1: TRowSet = { startRowOffset: new Int64(0), + rows: [], arrowBatches: [sampleArrowBatch], }; -const sampleRowSet1LZ4Compressed = { +const sampleRowSet1LZ4Compressed: TRowSet = { startRowOffset: new Int64(0), - arrowBatches: sampleRowSet1.arrowBatches.map((item) => ({ + rows: [], + arrowBatches: sampleRowSet1.arrowBatches?.map((item) => ({ ...item, batch: LZ4.encode(item.batch), })), }; -const sampleRowSet2 = { +const sampleRowSet2: TRowSet = { startRowOffset: new Int64(0), + rows: [], arrowBatches: undefined, }; -const sampleRowSet3 = { +const sampleRowSet3: TRowSet = { startRowOffset: new Int64(0), + rows: [], arrowBatches: [], }; -const sampleRowSet4 = { +const sampleRowSet4: TRowSet = { startRowOffset: new Int64(0), + rows: [], arrowBatches: [ { - batch: undefined, + batch: undefined as unknown as Buffer, rowCount: new Int64(0), }, ], @@ -60,22 +68,24 @@ const sampleRowSet4 = { describe('ArrowResultHandler', () => { it('should return data', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([sampleRowSet1]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([sampleRowSet1], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); const { batches } = await result.fetchNext({ limit: 10000 }); expect(await rowSetProvider.hasMore()).to.be.false; expect(await result.hasMore()).to.be.false; - const expectedBatches = sampleRowSet1.arrowBatches.map(({ batch }) => batch); + const expectedBatches = sampleRowSet1.arrowBatches?.map(({ batch }) => batch) ?? []; expect(batches).to.deep.eq([sampleArrowSchema, ...expectedBatches]); }); it('should handle LZ4 compressed data', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([sampleRowSet1LZ4Compressed]); - const result = new ArrowResultHandler(context, rowSetProvider, { + const rowSetProvider = new ResultsProviderStub([sampleRowSet1LZ4Compressed], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, arrowSchema: sampleArrowSchema, lz4Compressed: true, }); @@ -84,14 +94,16 @@ describe('ArrowResultHandler', () => { expect(await rowSetProvider.hasMore()).to.be.false; expect(await result.hasMore()).to.be.false; - const expectedBatches = sampleRowSet1.arrowBatches.map(({ batch }) => batch); + const expectedBatches = sampleRowSet1.arrowBatches?.map(({ batch }) => batch) ?? []; expect(batches).to.deep.eq([sampleArrowSchema, ...expectedBatches]); }); it('should not buffer any data', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([sampleRowSet1]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([sampleRowSet1], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await rowSetProvider.hasMore()).to.be.true; expect(await result.hasMore()).to.be.true; @@ -101,61 +113,76 @@ describe('ArrowResultHandler', () => { }); it('should return empty array if no data to process', async () => { - const context = {}; - const expectedResult = { batches: [], rowCount: 0, }; case1: { - const rowSetProvider = new ResultsProviderMock(); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq(expectedResult); expect(await result.hasMore()).to.be.false; } case2: { - const rowSetProvider = new ResultsProviderMock([sampleRowSet2]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([sampleRowSet2], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq(expectedResult); expect(await result.hasMore()).to.be.false; } case3: { - const rowSetProvider = new ResultsProviderMock([sampleRowSet3]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([sampleRowSet3], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq(expectedResult); expect(await result.hasMore()).to.be.false; } case4: { - const rowSetProvider = new ResultsProviderMock([sampleRowSet4]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub([sampleRowSet4], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq(expectedResult); expect(await result.hasMore()).to.be.false; } }); it('should return a proper row count in a batch', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([ - { - ...sampleRowSet1, - arrowBatches: [ - { - batch: Buffer.alloc(0), - rowCount: new Int64(2), - }, - { - batch: Buffer.alloc(0), - rowCount: new Int64(0), - }, - { - batch: Buffer.alloc(0), - rowCount: new Int64(3), - }, - ], - }, - ]); - const result = new ArrowResultHandler(context, rowSetProvider, { arrowSchema: sampleArrowSchema }); + const rowSetProvider = new ResultsProviderStub( + [ + { + ...sampleRowSet1, + arrowBatches: [ + { + batch: Buffer.alloc(0), + rowCount: new Int64(2), + }, + { + batch: Buffer.alloc(0), + rowCount: new Int64(0), + }, + { + batch: Buffer.alloc(0), + rowCount: new Int64(3), + }, + ], + }, + ], + undefined, + ); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + arrowSchema: sampleArrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); const { rowCount } = await result.fetchNext({ limit: 10000 }); expect(await rowSetProvider.hasMore()).to.be.false; @@ -164,10 +191,9 @@ describe('ArrowResultHandler', () => { }); it('should infer arrow schema from thrift schema', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([sampleRowSet2]); + const rowSetProvider = new ResultsProviderStub([sampleRowSet2], undefined); - const sampleThriftSchema = { + const sampleThriftSchema: TTableSchema = { columns: [ { columnName: '1', @@ -176,7 +202,6 @@ describe('ArrowResultHandler', () => { { primitiveEntry: { type: 3, - typeQualifiers: null, }, }, ], @@ -186,14 +211,18 @@ describe('ArrowResultHandler', () => { ], }; - const result = new ArrowResultHandler(context, rowSetProvider, { schema: sampleThriftSchema }); - expect(result.arrowSchema).to.not.be.undefined; + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + schema: sampleThriftSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + expect(result['arrowSchema']).to.not.be.undefined; }); it('should return empty array if no schema available', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock([sampleRowSet2]); - const result = new ArrowResultHandler(context, rowSetProvider, {}); + const rowSetProvider = new ResultsProviderStub([sampleRowSet2], undefined); + const result = new ArrowResultHandler(new ClientContextStub(), rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq({ batches: [], rowCount: 0, diff --git a/tests/unit/result/CloudFetchResultHandler.test.js b/tests/unit/result/CloudFetchResultHandler.test.js deleted file mode 100644 index e7e19eab..00000000 --- a/tests/unit/result/CloudFetchResultHandler.test.js +++ /dev/null @@ -1,358 +0,0 @@ -const { expect, AssertionError } = require('chai'); -const sinon = require('sinon'); -const Int64 = require('node-int64'); -const LZ4 = require('lz4'); -const CloudFetchResultHandler = require('../../../lib/result/CloudFetchResultHandler').default; -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); -const DBSQLClient = require('../../../lib/DBSQLClient').default; - -const sampleArrowSchema = Buffer.from([ - 255, 255, 255, 255, 208, 0, 0, 0, 16, 0, 0, 0, 0, 0, 10, 0, 14, 0, 6, 0, 13, 0, 8, 0, 10, 0, 0, 0, 0, 0, 4, 0, 16, 0, - 0, 0, 0, 1, 10, 0, 12, 0, 0, 0, 8, 0, 4, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, - 0, 0, 18, 0, 24, 0, 20, 0, 0, 0, 19, 0, 12, 0, 0, 0, 8, 0, 4, 0, 18, 0, 0, 0, 20, 0, 0, 0, 80, 0, 0, 0, 88, 0, 0, 0, - 0, 0, 0, 2, 92, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 3, 0, - 0, 0, 73, 78, 84, 0, 22, 0, 0, 0, 83, 112, 97, 114, 107, 58, 68, 97, 116, 97, 84, 121, 112, 101, 58, 83, 113, 108, 78, - 97, 109, 101, 0, 0, 0, 0, 0, 0, 8, 0, 12, 0, 8, 0, 7, 0, 8, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 1, 0, 0, 0, 49, 0, 0, 0, - 0, 0, 0, 0, -]); - -const sampleArrowBatch = Buffer.from([ - 255, 255, 255, 255, 136, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 12, 0, 22, 0, 14, 0, 21, 0, 16, 0, 4, 0, 12, 0, 0, 0, 16, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 16, 0, 0, 0, 0, 3, 10, 0, 24, 0, 12, 0, 8, 0, 4, 0, 10, 0, 0, 0, 20, 0, 0, 0, 56, 0, - 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, - 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, -]); - -const defaultLinkExpiryTime = Date.now() + 24 * 60 * 60 * 1000; // 24hr in future - -const sampleRowSet1 = { - startRowOffset: 0, - resultLinks: [ - { - fileLink: 'http://example.com/result/1', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - { - fileLink: 'http://example.com/result/2', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - ], -}; - -const sampleRowSet2 = { - startRowOffset: new Int64(0), - resultLinks: [ - { - fileLink: 'http://example.com/result/3', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - { - fileLink: 'http://example.com/result/4', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - { - fileLink: 'http://example.com/result/5', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - ], -}; - -const sampleEmptyRowSet = { - startRowOffset: new Int64(0), - resultLinks: undefined, -}; - -const sampleExpiredRowSet = { - startRowOffset: new Int64(0), - resultLinks: [ - { - fileLink: 'http://example.com/result/6', - expiryTime: new Int64(defaultLinkExpiryTime), - rowCount: new Int64(1), - }, - { - fileLink: 'http://example.com/result/7', - expiryTime: new Int64(Date.now() - 24 * 60 * 60 * 1000), // 24hr in past - rowCount: new Int64(1), - }, - ], -}; - -class ClientContextMock { - constructor(configOverrides) { - this.configOverrides = configOverrides; - this.fetchHandler = sinon.stub(); - - this.connectionProvider = { - getAgent: () => Promise.resolve(undefined), - getRetryPolicy: sinon.stub().returns( - Promise.resolve({ - shouldRetry: sinon.stub().returns(Promise.resolve({ shouldRetry: false })), - invokeWithRetry: sinon.stub().callsFake(() => this.fetchHandler().then((response) => ({ response }))), - }), - ), - }; - } - - getConfig() { - const defaultConfig = DBSQLClient.getDefaultConfig(); - return { - ...defaultConfig, - ...this.configOverrides, - }; - } - - getConnectionProvider() { - return Promise.resolve(this.connectionProvider); - } -} - -describe('CloudFetchResultHandler', () => { - it('should report pending data if there are any', async () => { - const context = new ClientContextMock({ cloudFetchConcurrentDownloads: 1 }); - const rowSetProvider = new ResultsProviderMock(); - - const result = new CloudFetchResultHandler(context, rowSetProvider, {}); - - case1: { - result.pendingLinks = []; - result.downloadTasks = []; - expect(await result.hasMore()).to.be.false; - } - - case2: { - result.pendingLinks = [{}]; // just anything here - result.downloadTasks = []; - expect(await result.hasMore()).to.be.true; - } - - case3: { - result.pendingLinks = []; - result.downloadTasks = [{}]; // just anything here - expect(await result.hasMore()).to.be.true; - } - }); - - it('should extract links from row sets', async () => { - const context = new ClientContextMock({ cloudFetchConcurrentDownloads: 0 }); - - const rowSets = [sampleRowSet1, sampleEmptyRowSet, sampleRowSet2]; - const expectedLinksCount = rowSets.reduce((prev, item) => prev + (item.resultLinks?.length ?? 0), 0); - - const rowSetProvider = new ResultsProviderMock(rowSets); - - const result = new CloudFetchResultHandler(context, rowSetProvider, {}); - - context.fetchHandler.returns( - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: async () => Buffer.concat([sampleArrowSchema, sampleArrowBatch]), - }), - ); - - do { - await result.fetchNext({ limit: 100000 }); - } while (await rowSetProvider.hasMore()); - - expect(result.pendingLinks.length).to.be.equal(expectedLinksCount); - expect(result.downloadTasks.length).to.be.equal(0); - expect(context.fetchHandler.called).to.be.false; - }); - - it('should download batches according to settings', async () => { - const context = new ClientContextMock({ cloudFetchConcurrentDownloads: 3 }); - const clientConfig = context.getConfig(); - - const rowSet = { - startRowOffset: new Int64(0), - resultLinks: [...sampleRowSet1.resultLinks, ...sampleRowSet2.resultLinks], - }; - const expectedLinksCount = rowSet.resultLinks.length; // 5 - const rowSetProvider = new ResultsProviderMock([rowSet]); - - const result = new CloudFetchResultHandler(context, rowSetProvider, {}); - - context.fetchHandler.returns( - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: async () => Buffer.concat([sampleArrowSchema, sampleArrowBatch]), - }), - ); - - expect(await rowSetProvider.hasMore()).to.be.true; - - initialFetch: { - // `cloudFetchConcurrentDownloads` out of `expectedLinksCount` links should be scheduled immediately - // first one should be `await`-ed and returned from `fetchNext` - const { batches } = await result.fetchNext({ limit: 10000 }); - expect(batches.length).to.be.gt(0); - expect(await rowSetProvider.hasMore()).to.be.false; - - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads); - expect(result.pendingLinks.length).to.be.equal(expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads); - expect(result.downloadTasks.length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); - } - - secondFetch: { - // It should return previously fetched batch, and schedule one more - const { batches } = await result.fetchNext({ limit: 10000 }); - expect(batches.length).to.be.gt(0); - expect(await rowSetProvider.hasMore()).to.be.false; - - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads + 1); - expect(result.pendingLinks.length).to.be.equal( - expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads - 1, - ); - expect(result.downloadTasks.length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); - } - - thirdFetch: { - // Now buffer should be empty, and it should fetch next batches - const { batches } = await result.fetchNext({ limit: 10000 }); - expect(batches.length).to.be.gt(0); - expect(await rowSetProvider.hasMore()).to.be.false; - - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads + 2); - expect(result.pendingLinks.length).to.be.equal( - expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads - 2, - ); - expect(result.downloadTasks.length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); - } - }); - - it('should return a proper row count in a batch', async () => { - const context = new ClientContextMock(); - - const rowSetProvider = new ResultsProviderMock([sampleRowSet1]); - - const result = new CloudFetchResultHandler(context, rowSetProvider, { lz4Compressed: false }); - - context.fetchHandler.returns( - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: async () => Buffer.alloc(0), - }), - ); - - expect(await rowSetProvider.hasMore()).to.be.true; - - const { rowCount } = await result.fetchNext({ limit: 10000 }); - expect(await rowSetProvider.hasMore()).to.be.false; - - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.called).to.be.true; - expect(rowCount).to.equal(1); - }); - - it('should handle LZ4 compressed data', async () => { - const context = new ClientContextMock(); - - const rowSetProvider = new ResultsProviderMock([sampleRowSet1]); - - const result = new CloudFetchResultHandler(context, rowSetProvider, { lz4Compressed: true }); - - const expectedBatch = Buffer.concat([sampleArrowSchema, sampleArrowBatch]); - - context.fetchHandler.returns( - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: async () => LZ4.encode(expectedBatch), - }), - ); - - expect(await rowSetProvider.hasMore()).to.be.true; - - const { batches } = await result.fetchNext({ limit: 10000 }); - expect(await rowSetProvider.hasMore()).to.be.false; - - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.called).to.be.true; - expect(batches).to.deep.eq([expectedBatch]); - }); - - it('should handle HTTP errors', async () => { - const context = new ClientContextMock({ cloudFetchConcurrentDownloads: 1 }); - - const rowSetProvider = new ResultsProviderMock([sampleRowSet1]); - - const result = new CloudFetchResultHandler(context, rowSetProvider, {}); - - context.fetchHandler.returns( - Promise.resolve({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - arrayBuffer: async () => Buffer.concat([sampleArrowSchema, sampleArrowBatch]), - }), - ); - - try { - await result.fetchNext({ limit: 10000 }); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(error.message).to.contain('Internal Server Error'); - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - expect(context.fetchHandler.callCount).to.be.equal(1); - } - }); - - it('should handle expired links', async () => { - const context = new ClientContextMock(); - const rowSetProvider = new ResultsProviderMock([sampleExpiredRowSet]); - - const result = new CloudFetchResultHandler(context, rowSetProvider, {}); - - context.fetchHandler.returns( - Promise.resolve({ - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: async () => Buffer.concat([sampleArrowSchema, sampleArrowBatch]), - }), - ); - - // There are two link in the batch - first one is valid and second one is expired - // The first fetch has to be successful, and the second one should fail - await result.fetchNext({ limit: 10000 }); - - try { - await result.fetchNext({ limit: 10000 }); - expect.fail('It should throw an error'); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - expect(error.message).to.contain('CloudFetch link has expired'); - // it should use retry policy for all requests - expect(context.connectionProvider.getRetryPolicy.called).to.be.true; - // Row set contains a one valid and one expired link; only valid link should be requested - expect(context.fetchHandler.callCount).to.be.equal(1); - } - }); -}); diff --git a/tests/unit/result/CloudFetchResultHandler.test.ts b/tests/unit/result/CloudFetchResultHandler.test.ts new file mode 100644 index 00000000..7927ee41 --- /dev/null +++ b/tests/unit/result/CloudFetchResultHandler.test.ts @@ -0,0 +1,382 @@ +import { expect, AssertionError } from 'chai'; +import sinon, { SinonStub } from 'sinon'; +import Int64 from 'node-int64'; +import LZ4 from 'lz4'; +import { Request, Response } from 'node-fetch'; +import { ShouldRetryResult } from '../../../lib/connection/contracts/IRetryPolicy'; +import { HttpTransactionDetails } from '../../../lib/connection/contracts/IConnectionProvider'; +import CloudFetchResultHandler from '../../../lib/result/CloudFetchResultHandler'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; +import { TRowSet, TSparkArrowResultLink, TStatusCode } from '../../../thrift/TCLIService_types'; +import { ArrowBatch } from '../../../lib/result/utils'; + +import BaseClientContextStub from '../.stubs/ClientContextStub'; +import { ClientConfig } from '../../../lib/contracts/IClientContext'; +import ConnectionProviderStub from '../.stubs/ConnectionProviderStub'; + +const sampleArrowSchema = Buffer.from([ + 255, 255, 255, 255, 208, 0, 0, 0, 16, 0, 0, 0, 0, 0, 10, 0, 14, 0, 6, 0, 13, 0, 8, 0, 10, 0, 0, 0, 0, 0, 4, 0, 16, 0, + 0, 0, 0, 1, 10, 0, 12, 0, 0, 0, 8, 0, 4, 0, 10, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 24, 0, 0, 0, + 0, 0, 18, 0, 24, 0, 20, 0, 0, 0, 19, 0, 12, 0, 0, 0, 8, 0, 4, 0, 18, 0, 0, 0, 20, 0, 0, 0, 80, 0, 0, 0, 88, 0, 0, 0, + 0, 0, 0, 2, 92, 0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 12, 0, 0, 0, 3, 0, + 0, 0, 73, 78, 84, 0, 22, 0, 0, 0, 83, 112, 97, 114, 107, 58, 68, 97, 116, 97, 84, 121, 112, 101, 58, 83, 113, 108, 78, + 97, 109, 101, 0, 0, 0, 0, 0, 0, 8, 0, 12, 0, 8, 0, 7, 0, 8, 0, 0, 0, 0, 0, 0, 1, 32, 0, 0, 0, 1, 0, 0, 0, 49, 0, 0, 0, + 0, 0, 0, 0, +]); + +const sampleArrowBatch = Buffer.from([ + 255, 255, 255, 255, 136, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 12, 0, 22, 0, 14, 0, 21, 0, 16, 0, 4, 0, 12, 0, 0, 0, 16, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 16, 0, 0, 0, 0, 3, 10, 0, 24, 0, 12, 0, 8, 0, 4, 0, 10, 0, 0, 0, 20, 0, 0, 0, 56, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, + 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, +]); + +const defaultLinkExpiryTime = Date.now() + 24 * 60 * 60 * 1000; // 24hr in future + +const sampleRowSet1: TRowSet = { + startRowOffset: new Int64(0), + rows: [], + resultLinks: [ + { + fileLink: 'http://example.com/result/1', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + { + fileLink: 'http://example.com/result/2', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + ], +}; + +const sampleRowSet2: TRowSet = { + startRowOffset: new Int64(0), + rows: [], + resultLinks: [ + { + fileLink: 'http://example.com/result/3', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + { + fileLink: 'http://example.com/result/4', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + { + fileLink: 'http://example.com/result/5', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + ], +}; + +const sampleEmptyRowSet: TRowSet = { + startRowOffset: new Int64(0), + rows: [], + resultLinks: undefined, +}; + +const sampleExpiredRowSet: TRowSet = { + startRowOffset: new Int64(0), + rows: [], + resultLinks: [ + { + fileLink: 'http://example.com/result/6', + expiryTime: new Int64(defaultLinkExpiryTime), + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + { + fileLink: 'http://example.com/result/7', + expiryTime: new Int64(Date.now() - 24 * 60 * 60 * 1000), // 24hr in past + rowCount: new Int64(1), + startRowOffset: new Int64(0), + bytesNum: new Int64(0), + }, + ], +}; + +class ClientContextStub extends BaseClientContextStub { + public connectionProvider = sinon.stub(new ConnectionProviderStub()); + + public invokeWithRetryStub = sinon.stub<[], Promise>(); + + constructor(configOverrides: Partial = {}) { + super(configOverrides); + + this.connectionProvider.getRetryPolicy.callsFake(async () => ({ + shouldRetry: async (): Promise => { + return { shouldRetry: false }; + }, + invokeWithRetry: async (): Promise => { + return this.invokeWithRetryStub(); + }, + })); + } +} + +describe('CloudFetchResultHandler', () => { + it('should report pending data if there are any', async () => { + const context = new ClientContextStub({ cloudFetchConcurrentDownloads: 1 }); + const rowSetProvider = new ResultsProviderStub([], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + case1: { + result['pendingLinks'] = []; + result['downloadTasks'] = []; + expect(await result.hasMore()).to.be.false; + } + + case2: { + result['pendingLinks'] = [ + { + fileLink: '', + expiryTime: new Int64(0), + startRowOffset: new Int64(0), + rowCount: new Int64(0), + bytesNum: new Int64(0), + }, + ]; + result['downloadTasks'] = []; + expect(await result.hasMore()).to.be.true; + } + + case3: { + result['pendingLinks'] = []; + result['downloadTasks'] = [ + Promise.resolve({ + batches: [], + rowCount: 0, + }), + ]; + expect(await result.hasMore()).to.be.true; + } + }); + + it('should extract links from row sets', async () => { + const context = new ClientContextStub({ cloudFetchConcurrentDownloads: 0 }); + + const rowSets = [sampleRowSet1, sampleEmptyRowSet, sampleRowSet2]; + const expectedLinksCount = rowSets.reduce((prev, item) => prev + (item.resultLinks?.length ?? 0), 0); + + const rowSetProvider = new ResultsProviderStub(rowSets, undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(Buffer.concat([sampleArrowSchema, sampleArrowBatch]), { status: 200 }), + })); + + do { + await result.fetchNext({ limit: 100000 }); + } while (await rowSetProvider.hasMore()); + + expect(result['pendingLinks'].length).to.be.equal(expectedLinksCount); + expect(result['downloadTasks'].length).to.be.equal(0); + expect(context.invokeWithRetryStub.called).to.be.false; + }); + + it('should download batches according to settings', async () => { + const context = new ClientContextStub({ cloudFetchConcurrentDownloads: 3 }); + const clientConfig = context.getConfig(); + + const rowSet: TRowSet = { + startRowOffset: new Int64(0), + rows: [], + resultLinks: [...(sampleRowSet1.resultLinks ?? []), ...(sampleRowSet2.resultLinks ?? [])], + }; + const expectedLinksCount = rowSet.resultLinks?.length ?? 0; // 5 + const rowSetProvider = new ResultsProviderStub([rowSet], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(Buffer.concat([sampleArrowSchema, sampleArrowBatch]), { status: 200 }), + })); + + expect(await rowSetProvider.hasMore()).to.be.true; + + initialFetch: { + // `cloudFetchConcurrentDownloads` out of `expectedLinksCount` links should be scheduled immediately + // first one should be `await`-ed and returned from `fetchNext` + const { batches } = await result.fetchNext({ limit: 10000 }); + expect(batches.length).to.be.gt(0); + expect(await rowSetProvider.hasMore()).to.be.false; + + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads); + expect(result['pendingLinks'].length).to.be.equal( + expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads, + ); + expect(result['downloadTasks'].length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); + } + + secondFetch: { + // It should return previously fetched batch, and schedule one more + const { batches } = await result.fetchNext({ limit: 10000 }); + expect(batches.length).to.be.gt(0); + expect(await rowSetProvider.hasMore()).to.be.false; + + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads + 1); + expect(result['pendingLinks'].length).to.be.equal( + expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads - 1, + ); + expect(result['downloadTasks'].length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); + } + + thirdFetch: { + // Now buffer should be empty, and it should fetch next batches + const { batches } = await result.fetchNext({ limit: 10000 }); + expect(batches.length).to.be.gt(0); + expect(await rowSetProvider.hasMore()).to.be.false; + + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.callCount).to.be.equal(clientConfig.cloudFetchConcurrentDownloads + 2); + expect(result['pendingLinks'].length).to.be.equal( + expectedLinksCount - clientConfig.cloudFetchConcurrentDownloads - 2, + ); + expect(result['downloadTasks'].length).to.be.equal(clientConfig.cloudFetchConcurrentDownloads - 1); + } + }); + + it('should return a proper row count in a batch', async () => { + const context = new ClientContextStub(); + + const rowSetProvider = new ResultsProviderStub([sampleRowSet1], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + lz4Compressed: false, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(Buffer.alloc(0), { status: 200 }), + })); + + expect(await rowSetProvider.hasMore()).to.be.true; + + const { rowCount } = await result.fetchNext({ limit: 10000 }); + expect(await rowSetProvider.hasMore()).to.be.false; + + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.called).to.be.true; + expect(rowCount).to.equal(1); + }); + + it('should handle LZ4 compressed data', async () => { + const context = new ClientContextStub(); + + const rowSetProvider = new ResultsProviderStub([sampleRowSet1], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + lz4Compressed: true, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + const expectedBatch = Buffer.concat([sampleArrowSchema, sampleArrowBatch]); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(LZ4.encode(expectedBatch), { status: 200 }), + })); + + expect(await rowSetProvider.hasMore()).to.be.true; + + const { batches } = await result.fetchNext({ limit: 10000 }); + expect(await rowSetProvider.hasMore()).to.be.false; + + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.called).to.be.true; + expect(batches).to.deep.eq([expectedBatch]); + }); + + it('should handle HTTP errors', async () => { + const context = new ClientContextStub({ cloudFetchConcurrentDownloads: 1 }); + + const rowSetProvider = new ResultsProviderStub([sampleRowSet1], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(Buffer.concat([sampleArrowSchema, sampleArrowBatch]), { status: 500 }), + })); + + try { + await result.fetchNext({ limit: 10000 }); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(error.message).to.contain('Internal Server Error'); + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + expect(context.invokeWithRetryStub.callCount).to.be.equal(1); + } + }); + + it('should handle expired links', async () => { + const context = new ClientContextStub(); + const rowSetProvider = new ResultsProviderStub([sampleExpiredRowSet], undefined); + + const result = new CloudFetchResultHandler(context, rowSetProvider, { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + + context.invokeWithRetryStub.callsFake(async () => ({ + request: new Request('localhost'), + response: new Response(Buffer.concat([sampleArrowSchema, sampleArrowBatch]), { status: 200 }), + })); + + // There are two link in the batch - first one is valid and second one is expired + // The first fetch has to be successful, and the second one should fail + await result.fetchNext({ limit: 10000 }); + + try { + await result.fetchNext({ limit: 10000 }); + expect.fail('It should throw an error'); + } catch (error) { + if (error instanceof AssertionError || !(error instanceof Error)) { + throw error; + } + expect(error.message).to.contain('CloudFetch link has expired'); + // it should use retry policy for all requests + expect((context.connectionProvider.getRetryPolicy as SinonStub).called).to.be.true; + // Row set contains a one valid and one expired link; only valid link should be requested + expect(context.invokeWithRetryStub.callCount).to.be.equal(1); + } + }); +}); diff --git a/tests/unit/result/JsonResultHandler.test.js b/tests/unit/result/JsonResultHandler.test.ts similarity index 50% rename from tests/unit/result/JsonResultHandler.test.js rename to tests/unit/result/JsonResultHandler.test.ts index 5cef3ac9..45ea1f34 100644 --- a/tests/unit/result/JsonResultHandler.test.js +++ b/tests/unit/result/JsonResultHandler.test.ts @@ -1,10 +1,12 @@ -const { expect } = require('chai'); -const JsonResultHandler = require('../../../lib/result/JsonResultHandler').default; -const { TCLIService_types } = require('../../../lib').thrift; -const Int64 = require('node-int64'); -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); +import { expect } from 'chai'; +import Int64 from 'node-int64'; +import JsonResultHandler from '../../../lib/result/JsonResultHandler'; +import { TColumnDesc, TRowSet, TStatusCode, TTableSchema, TTypeId } from '../../../thrift/TCLIService_types'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; -const getColumnSchema = (columnName, type, position) => { +import ClientContextStub from '../.stubs/ClientContextStub'; + +const getColumnSchema = (columnName: string, type: TTypeId | undefined, position: number): TColumnDesc => { if (type === undefined) { return { columnName, @@ -30,19 +32,23 @@ const getColumnSchema = (columnName, type, position) => { describe('JsonResultHandler', () => { it('should not buffer any data', async () => { - const schema = { - columns: [getColumnSchema('table.id', TCLIService_types.TTypeId.STRING_TYPE, 1)], + const schema: TTableSchema = { + columns: [getColumnSchema('table.id', TTypeId.STRING_TYPE, 1)], }; - const data = [ + const data: TRowSet[] = [ { - columns: [{ stringVal: { values: ['0', '1'] } }], + startRowOffset: new Int64(0), + rows: [], + columns: [{ stringVal: { values: ['0', '1'], nulls: Buffer.from([]) } }], }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await rowSetProvider.hasMore()).to.be.true; expect(await result.hasMore()).to.be.true; @@ -52,31 +58,33 @@ describe('JsonResultHandler', () => { }); it('should convert schema with primitive types to json', async () => { - const schema = { + const schema: TTableSchema = { columns: [ - getColumnSchema('table.str', TCLIService_types.TTypeId.STRING_TYPE, 1), - getColumnSchema('table.int64', TCLIService_types.TTypeId.BIGINT_TYPE, 2), - getColumnSchema('table.bin', TCLIService_types.TTypeId.BINARY_TYPE, 3), - getColumnSchema('table.bool', TCLIService_types.TTypeId.BOOLEAN_TYPE, 4), - getColumnSchema('table.char', TCLIService_types.TTypeId.CHAR_TYPE, 5), - getColumnSchema('table.dbl', TCLIService_types.TTypeId.DOUBLE_TYPE, 6), - getColumnSchema('table.flt', TCLIService_types.TTypeId.FLOAT_TYPE, 7), - getColumnSchema('table.int', TCLIService_types.TTypeId.INT_TYPE, 8), - getColumnSchema('table.small_int', TCLIService_types.TTypeId.SMALLINT_TYPE, 9), - getColumnSchema('table.tiny_int', TCLIService_types.TTypeId.TINYINT_TYPE, 10), - getColumnSchema('table.varch', TCLIService_types.TTypeId.VARCHAR_TYPE, 11), - getColumnSchema('table.dec', TCLIService_types.TTypeId.DECIMAL_TYPE, 12), - getColumnSchema('table.ts', TCLIService_types.TTypeId.TIMESTAMP_TYPE, 13), - getColumnSchema('table.date', TCLIService_types.TTypeId.DATE_TYPE, 14), - getColumnSchema('table.day_interval', TCLIService_types.TTypeId.INTERVAL_DAY_TIME_TYPE, 15), - getColumnSchema('table.month_interval', TCLIService_types.TTypeId.INTERVAL_YEAR_MONTH_TYPE, 16), + getColumnSchema('table.str', TTypeId.STRING_TYPE, 1), + getColumnSchema('table.int64', TTypeId.BIGINT_TYPE, 2), + getColumnSchema('table.bin', TTypeId.BINARY_TYPE, 3), + getColumnSchema('table.bool', TTypeId.BOOLEAN_TYPE, 4), + getColumnSchema('table.char', TTypeId.CHAR_TYPE, 5), + getColumnSchema('table.dbl', TTypeId.DOUBLE_TYPE, 6), + getColumnSchema('table.flt', TTypeId.FLOAT_TYPE, 7), + getColumnSchema('table.int', TTypeId.INT_TYPE, 8), + getColumnSchema('table.small_int', TTypeId.SMALLINT_TYPE, 9), + getColumnSchema('table.tiny_int', TTypeId.TINYINT_TYPE, 10), + getColumnSchema('table.varch', TTypeId.VARCHAR_TYPE, 11), + getColumnSchema('table.dec', TTypeId.DECIMAL_TYPE, 12), + getColumnSchema('table.ts', TTypeId.TIMESTAMP_TYPE, 13), + getColumnSchema('table.date', TTypeId.DATE_TYPE, 14), + getColumnSchema('table.day_interval', TTypeId.INTERVAL_DAY_TIME_TYPE, 15), + getColumnSchema('table.month_interval', TTypeId.INTERVAL_YEAR_MONTH_TYPE, 16), ], }; - const data = [ + const data: TRowSet[] = [ { + startRowOffset: new Int64(0), + rows: [], columns: [ { - stringVal: { values: ['a', 'b'] }, + stringVal: { values: ['a', 'b'], nulls: Buffer.from([]) }, }, { i64Val: { @@ -84,58 +92,64 @@ describe('JsonResultHandler', () => { new Int64(Buffer.from([0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01])), new Int64(Buffer.from([0x00, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02])), ], + nulls: Buffer.from([]), }, }, { - binaryVal: { values: [Buffer.from([1]), Buffer.from([2])] }, + binaryVal: { + values: [Buffer.from([1]), Buffer.from([2])], + nulls: Buffer.from([]), + }, }, { - boolVal: { values: [true, false] }, + boolVal: { values: [true, false], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['c', 'd'] }, + stringVal: { values: ['c', 'd'], nulls: Buffer.from([]) }, }, { - doubleVal: { values: [1.2, 1.3] }, + doubleVal: { values: [1.2, 1.3], nulls: Buffer.from([]) }, }, { - doubleVal: { values: [2.2, 2.3] }, + doubleVal: { values: [2.2, 2.3], nulls: Buffer.from([]) }, }, { - i32Val: { values: [1, 2] }, + i32Val: { values: [1, 2], nulls: Buffer.from([]) }, }, { - i16Val: { values: [3, 4] }, + i16Val: { values: [3, 4], nulls: Buffer.from([]) }, }, { - byteVal: { values: [5, 6] }, + byteVal: { values: [5, 6], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['e', 'f'] }, + stringVal: { values: ['e', 'f'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['2.1', '2.2'] }, + stringVal: { values: ['2.1', '2.2'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['2020-01-17 00:17:13.0', '2020-01-17 00:17:13.0'] }, + stringVal: { values: ['2020-01-17 00:17:13.0', '2020-01-17 00:17:13.0'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['2020-01-17', '2020-01-17'] }, + stringVal: { values: ['2020-01-17', '2020-01-17'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['1 00:00:00.000000000', '1 00:00:00.000000000'] }, + stringVal: { values: ['1 00:00:00.000000000', '1 00:00:00.000000000'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['0-1', '0-1'] }, + stringVal: { values: ['0-1', '0-1'], nulls: Buffer.from([]) }, }, ], }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([ { @@ -178,37 +192,44 @@ describe('JsonResultHandler', () => { }); it('should convert complex types', async () => { - const schema = { + const schema: TTableSchema = { columns: [ - getColumnSchema('table.array', TCLIService_types.TTypeId.ARRAY_TYPE, 1), - getColumnSchema('table.map', TCLIService_types.TTypeId.MAP_TYPE, 2), - getColumnSchema('table.struct', TCLIService_types.TTypeId.STRUCT_TYPE, 3), - getColumnSchema('table.union', TCLIService_types.TTypeId.UNION_TYPE, 4), + getColumnSchema('table.array', TTypeId.ARRAY_TYPE, 1), + getColumnSchema('table.map', TTypeId.MAP_TYPE, 2), + getColumnSchema('table.struct', TTypeId.STRUCT_TYPE, 3), + getColumnSchema('table.union', TTypeId.UNION_TYPE, 4), ], }; - const data = [ + const data: TRowSet[] = [ { + startRowOffset: new Int64(0), + rows: [], columns: [ { - stringVal: { values: ['["a", "b"]', '["c", "d"]'] }, + stringVal: { values: ['["a", "b"]', '["c", "d"]'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['{ "key": 12 }', '{ "key": 13 }'] }, + stringVal: { values: ['{ "key": 12 }', '{ "key": 13 }'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['{ "name": "Jon", "surname": "Doe" }', '{ "name": "Jane", "surname": "Doe" }'] }, + stringVal: { + values: ['{ "name": "Jon", "surname": "Doe" }', '{ "name": "Jane", "surname": "Doe" }'], + nulls: Buffer.from([]), + }, }, { - stringVal: { values: ['{0:12}', '{1:"foo"}'] }, + stringVal: { values: ['{0:12}', '{1:"foo"}'], nulls: Buffer.from([]) }, }, ], }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([ { @@ -227,10 +248,12 @@ describe('JsonResultHandler', () => { }); it('should detect nulls', () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock(); + const rowSetProvider = new ResultsProviderStub([], undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema: null }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema: undefined, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); const buf = Buffer.from([0x55, 0xaa, 0xc3]); [ @@ -259,33 +282,35 @@ describe('JsonResultHandler', () => { true, true, // 0xC3 ].forEach((value, i) => { - expect(result.isNull(buf, i), i).to.be.eq(value); + expect(result['isNull'](buf, i)).to.be.eq(value); }); }); it('should detect nulls for each type', async () => { - const schema = { + const schema: TTableSchema = { columns: [ - getColumnSchema('table.str', TCLIService_types.TTypeId.STRING_TYPE, 1), - getColumnSchema('table.int64', TCLIService_types.TTypeId.BIGINT_TYPE, 2), - getColumnSchema('table.bin', TCLIService_types.TTypeId.BINARY_TYPE, 3), - getColumnSchema('table.bool', TCLIService_types.TTypeId.BOOLEAN_TYPE, 4), - getColumnSchema('table.char', TCLIService_types.TTypeId.CHAR_TYPE, 5), - getColumnSchema('table.dbl', TCLIService_types.TTypeId.DOUBLE_TYPE, 6), - getColumnSchema('table.flt', TCLIService_types.TTypeId.FLOAT_TYPE, 7), - getColumnSchema('table.int', TCLIService_types.TTypeId.INT_TYPE, 8), - getColumnSchema('table.small_int', TCLIService_types.TTypeId.SMALLINT_TYPE, 9), - getColumnSchema('table.tiny_int', TCLIService_types.TTypeId.TINYINT_TYPE, 10), - getColumnSchema('table.varch', TCLIService_types.TTypeId.VARCHAR_TYPE, 11), - getColumnSchema('table.dec', TCLIService_types.TTypeId.DECIMAL_TYPE, 12), - getColumnSchema('table.ts', TCLIService_types.TTypeId.TIMESTAMP_TYPE, 13), - getColumnSchema('table.date', TCLIService_types.TTypeId.DATE_TYPE, 14), - getColumnSchema('table.day_interval', TCLIService_types.TTypeId.INTERVAL_DAY_TIME_TYPE, 15), - getColumnSchema('table.month_interval', TCLIService_types.TTypeId.INTERVAL_YEAR_MONTH_TYPE, 16), + getColumnSchema('table.str', TTypeId.STRING_TYPE, 1), + getColumnSchema('table.int64', TTypeId.BIGINT_TYPE, 2), + getColumnSchema('table.bin', TTypeId.BINARY_TYPE, 3), + getColumnSchema('table.bool', TTypeId.BOOLEAN_TYPE, 4), + getColumnSchema('table.char', TTypeId.CHAR_TYPE, 5), + getColumnSchema('table.dbl', TTypeId.DOUBLE_TYPE, 6), + getColumnSchema('table.flt', TTypeId.FLOAT_TYPE, 7), + getColumnSchema('table.int', TTypeId.INT_TYPE, 8), + getColumnSchema('table.small_int', TTypeId.SMALLINT_TYPE, 9), + getColumnSchema('table.tiny_int', TTypeId.TINYINT_TYPE, 10), + getColumnSchema('table.varch', TTypeId.VARCHAR_TYPE, 11), + getColumnSchema('table.dec', TTypeId.DECIMAL_TYPE, 12), + getColumnSchema('table.ts', TTypeId.TIMESTAMP_TYPE, 13), + getColumnSchema('table.date', TTypeId.DATE_TYPE, 14), + getColumnSchema('table.day_interval', TTypeId.INTERVAL_DAY_TIME_TYPE, 15), + getColumnSchema('table.month_interval', TTypeId.INTERVAL_YEAR_MONTH_TYPE, 16), ], }; - const data = [ + const data: TRowSet[] = [ { + startRowOffset: new Int64(0), + rows: [], columns: [ { stringVal: { values: ['a'], nulls: Buffer.from([0x01]) }, @@ -342,10 +367,12 @@ describe('JsonResultHandler', () => { }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([ { @@ -370,38 +397,44 @@ describe('JsonResultHandler', () => { }); it('should return empty array if no data to process', async () => { - const schema = { - columns: [getColumnSchema('table.id', TCLIService_types.TTypeId.STRING_TYPE, 1)], + const schema: TTableSchema = { + columns: [getColumnSchema('table.id', TTypeId.STRING_TYPE, 1)], }; - const context = {}; - const rowSetProvider = new ResultsProviderMock(); + const rowSetProvider = new ResultsProviderStub([], undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([]); }); it('should return empty array if no schema available', async () => { - const data = [ + const data: TRowSet[] = [ { + startRowOffset: new Int64(0), + rows: [], columns: [ { - stringVal: { values: ['0', '1'] }, + stringVal: { values: ['0', '1'], nulls: Buffer.from([]) }, }, ], }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, {}); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema: undefined, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([]); }); it('should return raw data if types are not specified', async () => { - const schema = { + const schema: TTableSchema = { columns: [ getColumnSchema('table.array', undefined, 1), getColumnSchema('table.map', undefined, 2), @@ -409,29 +442,36 @@ describe('JsonResultHandler', () => { getColumnSchema('table.union', undefined, 4), ], }; - const data = [ + const data: TRowSet[] = [ { + startRowOffset: new Int64(0), + rows: [], columns: [ { - stringVal: { values: ['["a", "b"]', '["c", "d"]'] }, + stringVal: { values: ['["a", "b"]', '["c", "d"]'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['{ "key": 12 }', '{ "key": 13 }'] }, + stringVal: { values: ['{ "key": 12 }', '{ "key": 13 }'], nulls: Buffer.from([]) }, }, { - stringVal: { values: ['{ "name": "Jon", "surname": "Doe" }', '{ "name": "Jane", "surname": "Doe" }'] }, + stringVal: { + values: ['{ "name": "Jon", "surname": "Doe" }', '{ "name": "Jane", "surname": "Doe" }'], + nulls: Buffer.from([]), + }, }, { - stringVal: { values: ['{0:12}', '{1:"foo"}'] }, + stringVal: { values: ['{0:12}', '{1:"foo"}'], nulls: Buffer.from([]) }, }, ], }, ]; - const context = {}; - const rowSetProvider = new ResultsProviderMock(data); + const rowSetProvider = new ResultsProviderStub(data, undefined); - const result = new JsonResultHandler(context, rowSetProvider, { schema }); + const result = new JsonResultHandler(new ClientContextStub(), rowSetProvider, { + schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); expect(await result.fetchNext({ limit: 10000 })).to.be.deep.eq([ { diff --git a/tests/unit/result/ResultSlicer.test.js b/tests/unit/result/ResultSlicer.test.ts similarity index 75% rename from tests/unit/result/ResultSlicer.test.js rename to tests/unit/result/ResultSlicer.test.ts index e00615d9..eeda3821 100644 --- a/tests/unit/result/ResultSlicer.test.js +++ b/tests/unit/result/ResultSlicer.test.ts @@ -1,11 +1,13 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const ResultSlicer = require('../../../lib/result/ResultSlicer').default; -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); +import { expect } from 'chai'; +import sinon, { SinonSpy } from 'sinon'; +import ResultSlicer from '../../../lib/result/ResultSlicer'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; + +import ClientContextStub from '../.stubs/ClientContextStub'; describe('ResultSlicer', () => { it('should return chunks of requested size', async () => { - const provider = new ResultsProviderMock( + const provider = new ResultsProviderStub( [ [10, 11, 12, 13, 14, 15], [20, 21, 22, 23, 24, 25], @@ -14,7 +16,7 @@ describe('ResultSlicer', () => { [], ); - const slicer = new ResultSlicer({}, provider); + const slicer = new ResultSlicer(new ClientContextStub(), provider); const chunk1 = await slicer.fetchNext({ limit: 4 }); expect(chunk1).to.deep.eq([10, 11, 12, 13]); @@ -30,7 +32,7 @@ describe('ResultSlicer', () => { }); it('should return raw chunks', async () => { - const provider = new ResultsProviderMock( + const provider = new ResultsProviderStub( [ [10, 11, 12, 13, 14, 15], [20, 21, 22, 23, 24, 25], @@ -40,21 +42,21 @@ describe('ResultSlicer', () => { ); sinon.spy(provider, 'fetchNext'); - const slicer = new ResultSlicer({}, provider); + const slicer = new ResultSlicer(new ClientContextStub(), provider); const chunk1 = await slicer.fetchNext({ limit: 4, disableBuffering: true }); expect(chunk1).to.deep.eq([10, 11, 12, 13, 14, 15]); expect(await slicer.hasMore()).to.be.true; - expect(provider.fetchNext.callCount).to.be.equal(1); + expect((provider.fetchNext as SinonSpy).callCount).to.be.equal(1); const chunk2 = await slicer.fetchNext({ limit: 10, disableBuffering: true }); expect(chunk2).to.deep.eq([20, 21, 22, 23, 24, 25]); expect(await slicer.hasMore()).to.be.true; - expect(provider.fetchNext.callCount).to.be.equal(2); + expect((provider.fetchNext as SinonSpy).callCount).to.be.equal(2); }); it('should switch between returning sliced and raw chunks', async () => { - const provider = new ResultsProviderMock( + const provider = new ResultsProviderStub( [ [10, 11, 12, 13, 14, 15], [20, 21, 22, 23, 24, 25], @@ -63,7 +65,7 @@ describe('ResultSlicer', () => { [], ); - const slicer = new ResultSlicer({}, provider); + const slicer = new ResultSlicer(new ClientContextStub(), provider); const chunk1 = await slicer.fetchNext({ limit: 4 }); expect(chunk1).to.deep.eq([10, 11, 12, 13]); diff --git a/tests/unit/result/compatibility.test.js b/tests/unit/result/compatibility.test.js deleted file mode 100644 index b232aa49..00000000 --- a/tests/unit/result/compatibility.test.js +++ /dev/null @@ -1,57 +0,0 @@ -const { expect } = require('chai'); -const ArrowResultHandler = require('../../../lib/result/ArrowResultHandler').default; -const ArrowResultConverter = require('../../../lib/result/ArrowResultConverter').default; -const JsonResultHandler = require('../../../lib/result/JsonResultHandler').default; - -const { fixArrowResult } = require('../../fixtures/compatibility'); -const fixtureColumn = require('../../fixtures/compatibility/column'); -const fixtureArrow = require('../../fixtures/compatibility/arrow'); -const fixtureArrowNT = require('../../fixtures/compatibility/arrow_native_types'); - -const ResultsProviderMock = require('./fixtures/ResultsProviderMock'); - -describe('Result handlers compatibility tests', () => { - it('colum-based data', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock(fixtureColumn.rowSets); - const result = new JsonResultHandler(context, rowSetProvider, { schema: fixtureColumn.schema }); - const rows = await result.fetchNext({ limit: 10000 }); - expect(rows).to.deep.equal(fixtureColumn.expected); - }); - - it('arrow-based data without native types', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock(fixtureArrow.rowSets); - const result = new ArrowResultConverter( - context, - new ArrowResultHandler(context, rowSetProvider, { arrowSchema: fixtureArrow.arrowSchema }), - { schema: fixtureArrow.schema }, - ); - const rows = await result.fetchNext({ limit: 10000 }); - expect(fixArrowResult(rows)).to.deep.equal(fixtureArrow.expected); - }); - - it('arrow-based data with native types', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock(fixtureArrowNT.rowSets); - const result = new ArrowResultConverter( - context, - new ArrowResultHandler(context, rowSetProvider, { arrowSchema: fixtureArrowNT.arrowSchema }), - { schema: fixtureArrowNT.schema }, - ); - const rows = await result.fetchNext({ limit: 10000 }); - expect(fixArrowResult(rows)).to.deep.equal(fixtureArrowNT.expected); - }); - - it('should infer arrow schema from thrift schema', async () => { - const context = {}; - const rowSetProvider = new ResultsProviderMock(fixtureArrow.rowSets); - const result = new ArrowResultConverter( - context, - new ArrowResultHandler(context, rowSetProvider, { schema: fixtureArrow.schema }), - { schema: fixtureArrow.schema }, - ); - const rows = await result.fetchNext({ limit: 10000 }); - expect(fixArrowResult(rows)).to.deep.equal(fixtureArrow.expected); - }); -}); diff --git a/tests/unit/result/compatibility.test.ts b/tests/unit/result/compatibility.test.ts new file mode 100644 index 00000000..cc6d89d8 --- /dev/null +++ b/tests/unit/result/compatibility.test.ts @@ -0,0 +1,71 @@ +import { expect } from 'chai'; +import { TStatusCode } from '../../../thrift/TCLIService_types'; +import ArrowResultHandler from '../../../lib/result/ArrowResultHandler'; +import ArrowResultConverter from '../../../lib/result/ArrowResultConverter'; +import JsonResultHandler from '../../../lib/result/JsonResultHandler'; +import ResultsProviderStub from '../.stubs/ResultsProviderStub'; + +import ClientContextStub from '../.stubs/ClientContextStub'; + +import { fixArrowResult } from '../../fixtures/compatibility'; +import * as fixtureColumn from '../../fixtures/compatibility/column'; +import * as fixtureArrow from '../../fixtures/compatibility/arrow'; +import * as fixtureArrowNT from '../../fixtures/compatibility/arrow_native_types'; + +describe('Result handlers compatibility tests', () => { + it('colum-based data', async () => { + const context = new ClientContextStub(); + const rowSetProvider = new ResultsProviderStub(fixtureColumn.rowSets, undefined); + const result = new JsonResultHandler(context, rowSetProvider, { + schema: fixtureColumn.schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }); + const rows = await result.fetchNext({ limit: 10000 }); + expect(rows).to.deep.equal(fixtureColumn.expected); + }); + + it('arrow-based data without native types', async () => { + const context = new ClientContextStub(); + const rowSetProvider = new ResultsProviderStub(fixtureArrow.rowSets, undefined); + const result = new ArrowResultConverter( + context, + new ArrowResultHandler(context, rowSetProvider, { + arrowSchema: fixtureArrow.arrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }), + { schema: fixtureArrow.schema, status: { statusCode: TStatusCode.SUCCESS_STATUS } }, + ); + const rows = await result.fetchNext({ limit: 10000 }); + expect(fixArrowResult(rows)).to.deep.equal(fixtureArrow.expected); + }); + + it('arrow-based data with native types', async () => { + const context = new ClientContextStub(); + const rowSetProvider = new ResultsProviderStub(fixtureArrowNT.rowSets, undefined); + const result = new ArrowResultConverter( + context, + new ArrowResultHandler(context, rowSetProvider, { + arrowSchema: fixtureArrowNT.arrowSchema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }), + { schema: fixtureArrowNT.schema, status: { statusCode: TStatusCode.SUCCESS_STATUS } }, + ); + const rows = await result.fetchNext({ limit: 10000 }); + expect(fixArrowResult(rows)).to.deep.equal(fixtureArrowNT.expected); + }); + + it('should infer arrow schema from thrift schema', async () => { + const context = new ClientContextStub(); + const rowSetProvider = new ResultsProviderStub(fixtureArrow.rowSets, undefined); + const result = new ArrowResultConverter( + context, + new ArrowResultHandler(context, rowSetProvider, { + schema: fixtureArrow.schema, + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + }), + { schema: fixtureArrow.schema, status: { statusCode: TStatusCode.SUCCESS_STATUS } }, + ); + const rows = await result.fetchNext({ limit: 10000 }); + expect(fixArrowResult(rows)).to.deep.equal(fixtureArrow.expected); + }); +}); diff --git a/tests/unit/result/fixtures/ResultsProviderMock.js b/tests/unit/result/fixtures/ResultsProviderMock.js deleted file mode 100644 index a1dba3e0..00000000 --- a/tests/unit/result/fixtures/ResultsProviderMock.js +++ /dev/null @@ -1,16 +0,0 @@ -class ResultsProviderMock { - constructor(items, emptyItem) { - this.items = Array.isArray(items) ? [...items] : []; - this.emptyItem = emptyItem; - } - - async hasMore() { - return this.items.length > 0; - } - - async fetchNext() { - return this.items.shift() ?? this.emptyItem; - } -} - -module.exports = ResultsProviderMock; diff --git a/tests/unit/result/fixtures/thriftSchemaAllNulls.json b/tests/unit/result/fixtures/thriftSchemaAllNulls.json deleted file mode 100644 index 8588f0a8..00000000 --- a/tests/unit/result/fixtures/thriftSchemaAllNulls.json +++ /dev/null @@ -1,318 +0,0 @@ -{ - "columns": [ - { - "columnName": "boolean_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 0, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 1, - "comment": "" - }, - { - "columnName": "tinyint_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 1, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 2, - "comment": "" - }, - { - "columnName": "smallint_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 2, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 3, - "comment": "" - }, - { - "columnName": "int_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 3, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 4, - "comment": "" - }, - { - "columnName": "bigint_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 4, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 5, - "comment": "" - }, - { - "columnName": "float_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 5, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 6, - "comment": "" - }, - { - "columnName": "double_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 6, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 7, - "comment": "" - }, - { - "columnName": "decimal_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { - "type": 15, - "typeQualifiers": { - "qualifiers": { - "scale": { "i32Value": 2, "stringValue": null }, - "precision": { "i32Value": 6, "stringValue": null } - } - } - }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 8, - "comment": "" - }, - { - "columnName": "string_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 7, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 9, - "comment": "" - }, - { - "columnName": "char_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 7, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 10, - "comment": "" - }, - { - "columnName": "varchar_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 7, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 11, - "comment": "" - }, - { - "columnName": "timestamp_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 8, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 12, - "comment": "" - }, - { - "columnName": "date_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 17, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 13, - "comment": "" - }, - { - "columnName": "day_interval_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 7, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 14, - "comment": "" - }, - { - "columnName": "month_interval_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 7, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 15, - "comment": "" - }, - { - "columnName": "binary_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 9, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 16, - "comment": "" - }, - { - "columnName": "struct_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 12, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 17, - "comment": "" - }, - { - "columnName": "array_field", - "typeDesc": { - "types": [ - { - "primitiveEntry": { "type": 10, "typeQualifiers": null }, - "arrayEntry": null, - "mapEntry": null, - "structEntry": null, - "unionEntry": null, - "userDefinedTypeEntry": null - } - ] - }, - "position": 18, - "comment": "" - } - ] -} diff --git a/tests/unit/result/utils.test.js b/tests/unit/result/utils.test.ts similarity index 84% rename from tests/unit/result/utils.test.js rename to tests/unit/result/utils.test.ts index 52604ca2..9097f5fe 100644 --- a/tests/unit/result/utils.test.js +++ b/tests/unit/result/utils.test.ts @@ -1,9 +1,7 @@ -const { expect } = require('chai'); -const Int64 = require('node-int64'); -const { TCLIService_types } = require('../../../lib').thrift; -const { getSchemaColumns, convertThriftValue } = require('../../../lib/result/utils'); - -const { TTypeId } = TCLIService_types; +import { expect } from 'chai'; +import Int64 from 'node-int64'; +import { TColumnDesc, TTypeDesc, TTypeId } from '../../../thrift/TCLIService_types'; +import { convertThriftValue, getSchemaColumns } from '../../../lib/result/utils'; describe('getSchemaColumns', () => { it('should handle missing schema', () => { @@ -12,9 +10,13 @@ describe('getSchemaColumns', () => { }); it('should return ordered columns', () => { - const columnA = { columnName: 'a', position: 2 }; - const columnB = { columnName: 'b', position: 3 }; - const columnC = { columnName: 'c', position: 1 }; + const typeDesc: TTypeDesc = { + types: [{ primitiveEntry: { type: TTypeId.STRING_TYPE } }], + }; + + const columnA: TColumnDesc = { columnName: 'a', position: 2, typeDesc }; + const columnB: TColumnDesc = { columnName: 'b', position: 3, typeDesc }; + const columnC: TColumnDesc = { columnName: 'c', position: 1, typeDesc }; const result = getSchemaColumns({ columns: [columnA, columnB, columnC], @@ -128,7 +130,7 @@ describe('convertThriftValue', () => { it('should return value if type is not recognized', () => { const value = 'test'; - const result = convertThriftValue({ type: null }, value); + const result = convertThriftValue({ type: -999 as TTypeId }, value); expect(result).to.equal(value); }); }); diff --git a/tests/unit/utils/CloseableCollection.test.js b/tests/unit/utils/CloseableCollection.test.ts similarity index 52% rename from tests/unit/utils/CloseableCollection.test.js rename to tests/unit/utils/CloseableCollection.test.ts index 3a167cf1..da7ab147 100644 --- a/tests/unit/utils/CloseableCollection.test.js +++ b/tests/unit/utils/CloseableCollection.test.ts @@ -1,98 +1,110 @@ -const { expect, AssertionError } = require('chai'); -const CloseableCollection = require('../../../lib/utils/CloseableCollection').default; +import { expect, AssertionError } from 'chai'; +import CloseableCollection, { ICloseable } from '../../../lib/utils/CloseableCollection'; describe('CloseableCollection', () => { it('should add item if not already added', () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); - const item = {}; + const item: ICloseable = { + close: () => Promise.resolve(), + }; collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); }); it('should add item if it is already added', () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); - const item = {}; + const item: ICloseable = { + close: () => Promise.resolve(), + }; collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); }); it('should delete item if already added', () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); - const item = {}; + const item: ICloseable = { + close: () => Promise.resolve(), + }; collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); collection.delete(item); expect(item.onClose).to.be.undefined; - expect(collection.items.size).to.be.eq(0); + expect(collection['items'].size).to.be.eq(0); }); it('should delete item if not added', () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); + + const item: ICloseable = { + close: () => Promise.resolve(), + }; - const item = {}; collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); - const otherItem = { onClose: () => {} }; + const otherItem: ICloseable = { + onClose: () => {}, + close: () => Promise.resolve(), + }; collection.delete(otherItem); // if item is not in collection - it should be just skipped expect(otherItem.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); }); it('should delete item if it was closed', async () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); - const item = { + const item: ICloseable = { close() { - this.onClose(); + this.onClose?.(); return Promise.resolve(); }, }; collection.add(item); expect(item.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(1); + expect(collection['items'].size).to.be.eq(1); await item.close(); expect(item.onClose).to.be.undefined; - expect(collection.items.size).to.be.eq(0); + expect(collection['items'].size).to.be.eq(0); }); it('should close all and delete all items', async () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); - const item1 = { + const item1: ICloseable = { close() { - this.onClose(); + this.onClose?.(); return Promise.resolve(); }, }; - const item2 = { + const item2: ICloseable = { close() { - this.onClose(); + this.onClose?.(); return Promise.resolve(); }, }; @@ -101,37 +113,37 @@ describe('CloseableCollection', () => { collection.add(item2); expect(item1.onClose).to.be.not.undefined; expect(item2.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(2); + expect(collection['items'].size).to.be.eq(2); await collection.closeAll(); expect(item1.onClose).to.be.undefined; expect(item2.onClose).to.be.undefined; - expect(collection.items.size).to.be.eq(0); + expect(collection['items'].size).to.be.eq(0); }); it('should close all and delete only first successfully closed items', async () => { - const collection = new CloseableCollection(); - expect(collection.items.size).to.be.eq(0); + const collection = new CloseableCollection(); + expect(collection['items'].size).to.be.eq(0); const errorMessage = 'Error from item 2'; - const item1 = { + const item1: ICloseable = { close() { - this.onClose(); + this.onClose?.(); return Promise.resolve(); }, }; - const item2 = { + const item2: ICloseable = { close() { // Item should call `.onClose` only if it was successfully closed return Promise.reject(new Error(errorMessage)); }, }; - const item3 = { + const item3: ICloseable = { close() { - this.onClose(); + this.onClose?.(); return Promise.resolve(); }, }; @@ -142,20 +154,20 @@ describe('CloseableCollection', () => { expect(item1.onClose).to.be.not.undefined; expect(item2.onClose).to.be.not.undefined; expect(item3.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(3); + expect(collection['items'].size).to.be.eq(3); try { await collection.closeAll(); expect.fail('It should throw an error'); } catch (error) { - if (error instanceof AssertionError) { + if (error instanceof AssertionError || !(error instanceof Error)) { throw error; } expect(error.message).to.eq(errorMessage); expect(item1.onClose).to.be.undefined; expect(item2.onClose).to.be.not.undefined; expect(item3.onClose).to.be.not.undefined; - expect(collection.items.size).to.be.eq(2); + expect(collection['items'].size).to.be.eq(2); } }); }); diff --git a/tests/unit/utils/OperationIterator.test.js b/tests/unit/utils/OperationIterator.test.ts similarity index 77% rename from tests/unit/utils/OperationIterator.test.js rename to tests/unit/utils/OperationIterator.test.ts index 29b82929..57632ff3 100644 --- a/tests/unit/utils/OperationIterator.test.js +++ b/tests/unit/utils/OperationIterator.test.ts @@ -1,40 +1,13 @@ -const { expect } = require('chai'); -const { OperationChunksIterator, OperationRowsIterator } = require('../../../lib/utils/OperationIterator'); - -class OperationMock { - // `chunks` should be an array of chunks - // where each chunk is an array of values - constructor(chunks) { - this.chunks = Array.isArray(chunks) ? [...chunks] : []; - this.closed = false; - } - - async hasMoreRows() { - return !this.closed && this.chunks.length > 0; - } - - async fetchChunk() { - return this.chunks.shift() ?? []; - } - - async close() { - this.closed = true; - } - - iterateChunks(options) { - return new OperationChunksIterator(this, options); - } - - iterateRows(options) { - return new OperationRowsIterator(this, options); - } -} +import { expect } from 'chai'; +import { OperationChunksIterator, OperationRowsIterator } from '../../../lib/utils/OperationIterator'; + +import OperationStub from '../.stubs/OperationStub'; describe('OperationChunksIterator', () => { it('should iterate over all chunks', async () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -51,7 +24,7 @@ describe('OperationChunksIterator', () => { it('should iterate over all chunks and close operation', async () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -68,7 +41,7 @@ describe('OperationChunksIterator', () => { it('should iterate partially', async () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -89,7 +62,7 @@ describe('OperationChunksIterator', () => { it('should iterate partially and close operation', async () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -108,7 +81,7 @@ describe('OperationRowsIterator', () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; const rows = chunks.flat(); - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -126,7 +99,7 @@ describe('OperationRowsIterator', () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; const rows = chunks.flat(); - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -143,7 +116,7 @@ describe('OperationRowsIterator', () => { it('should iterate partially', async () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; @@ -170,7 +143,7 @@ describe('OperationRowsIterator', () => { const chunks = [[1, 2, 3], [4, 5, 6, 7, 8], [9]]; const rows = chunks.flat(); - const operation = new OperationMock(chunks); + const operation = new OperationStub(chunks); expect(operation.closed).to.be.false; diff --git a/tests/unit/utils/utils.test.js b/tests/unit/utils/utils.test.ts similarity index 75% rename from tests/unit/utils/utils.test.js rename to tests/unit/utils/utils.test.ts index 67b3dfe2..f94e88c4 100644 --- a/tests/unit/utils/utils.test.js +++ b/tests/unit/utils/utils.test.ts @@ -1,11 +1,18 @@ -const { expect } = require('chai'); +import { expect } from 'chai'; +import Int64 from 'node-int64'; -const { - buildUserAgentString, - definedOrError, - formatProgress, - ProgressUpdateTransformer, -} = require('../../../lib/utils'); +import { TJobExecutionStatus, TProgressUpdateResp } from '../../../thrift/TCLIService_types'; + +import { buildUserAgentString, definedOrError, formatProgress, ProgressUpdateTransformer } from '../../../lib/utils'; + +const progressUpdateResponseStub: TProgressUpdateResp = { + headerNames: [], + rows: [], + progressedPercentage: 0, + status: TJobExecutionStatus.NOT_AVAILABLE, + footerSummary: '', + startTime: new Int64(0), +}; describe('buildUserAgentString', () => { // It should follow https://www.rfc-editor.org/rfc/rfc7231#section-5.5.3 and @@ -19,7 +26,7 @@ describe('buildUserAgentString', () => { // - with provided: NodejsDatabricksSqlConnector/0.1.8-beta.1 (Client ID; Node.js 16.13.1; Darwin 21.5.0) // - without provided: NodejsDatabricksSqlConnector/0.1.8-beta.1 (Node.js 16.13.1; Darwin 21.5.0) - function checkUserAgentString(ua, clientId) { + function checkUserAgentString(ua: string, clientId?: string) { // Prefix: 'NodejsDatabricksSqlConnector/' // Version: three period-separated digits and optional suffix const re = @@ -27,12 +34,12 @@ describe('buildUserAgentString', () => { const match = re.exec(ua); expect(match).to.not.be.eq(null); - const { comment } = match.groups; + const { comment } = match?.groups ?? {}; expect(comment.split(';').length).to.be.gte(2); // at least Node and OS version should be there if (clientId) { - expect(comment.trim()).to.satisfy((s) => s.startsWith(`${clientId};`)); + expect(comment.trim()).to.satisfy((s: string) => s.startsWith(`${clientId};`)); } } @@ -50,23 +57,21 @@ describe('buildUserAgentString', () => { describe('formatProgress', () => { it('formats progress', () => { - const result = formatProgress({ - headerNames: [], - rows: [], - }); + const result = formatProgress(progressUpdateResponseStub); expect(result).to.be.eq('\n'); }); }); describe('ProgressUpdateTransformer', () => { it('should have equal columns', () => { - const t = new ProgressUpdateTransformer(); + const t = new ProgressUpdateTransformer(progressUpdateResponseStub); expect(t.formatRow(['Column 1', 'Column 2'])).to.be.eq('Column 1 |Column 2 '); }); it('should format response as table', () => { const t = new ProgressUpdateTransformer({ + ...progressUpdateResponseStub, headerNames: ['Column 1', 'Column 2'], rows: [ ['value 1.1', 'value 1.2'], diff --git a/thrift/TCLIService.d.ts b/thrift/TCLIService.d.ts index 2f139936..5e254361 100644 --- a/thrift/TCLIService.d.ts +++ b/thrift/TCLIService.d.ts @@ -143,88 +143,46 @@ declare class Client { constructor(output: thrift.TTransport, pClass: { new(trans: thrift.TTransport): thrift.TProtocol }); - OpenSession(req: TOpenSessionReq): TOpenSessionResp; - OpenSession(req: TOpenSessionReq, callback?: (error: void, response: TOpenSessionResp)=>void): void; - CloseSession(req: TCloseSessionReq): TCloseSessionResp; - CloseSession(req: TCloseSessionReq, callback?: (error: void, response: TCloseSessionResp)=>void): void; - GetInfo(req: TGetInfoReq): TGetInfoResp; - GetInfo(req: TGetInfoReq, callback?: (error: void, response: TGetInfoResp)=>void): void; - ExecuteStatement(req: TExecuteStatementReq): TExecuteStatementResp; - ExecuteStatement(req: TExecuteStatementReq, callback?: (error: void, response: TExecuteStatementResp)=>void): void; - GetTypeInfo(req: TGetTypeInfoReq): TGetTypeInfoResp; - GetTypeInfo(req: TGetTypeInfoReq, callback?: (error: void, response: TGetTypeInfoResp)=>void): void; - GetCatalogs(req: TGetCatalogsReq): TGetCatalogsResp; - GetCatalogs(req: TGetCatalogsReq, callback?: (error: void, response: TGetCatalogsResp)=>void): void; - GetSchemas(req: TGetSchemasReq): TGetSchemasResp; - GetSchemas(req: TGetSchemasReq, callback?: (error: void, response: TGetSchemasResp)=>void): void; - GetTables(req: TGetTablesReq): TGetTablesResp; - GetTables(req: TGetTablesReq, callback?: (error: void, response: TGetTablesResp)=>void): void; - GetTableTypes(req: TGetTableTypesReq): TGetTableTypesResp; - GetTableTypes(req: TGetTableTypesReq, callback?: (error: void, response: TGetTableTypesResp)=>void): void; - GetColumns(req: TGetColumnsReq): TGetColumnsResp; - GetColumns(req: TGetColumnsReq, callback?: (error: void, response: TGetColumnsResp)=>void): void; - GetFunctions(req: TGetFunctionsReq): TGetFunctionsResp; - GetFunctions(req: TGetFunctionsReq, callback?: (error: void, response: TGetFunctionsResp)=>void): void; - GetPrimaryKeys(req: TGetPrimaryKeysReq): TGetPrimaryKeysResp; - GetPrimaryKeys(req: TGetPrimaryKeysReq, callback?: (error: void, response: TGetPrimaryKeysResp)=>void): void; - GetCrossReference(req: TGetCrossReferenceReq): TGetCrossReferenceResp; - GetCrossReference(req: TGetCrossReferenceReq, callback?: (error: void, response: TGetCrossReferenceResp)=>void): void; - GetOperationStatus(req: TGetOperationStatusReq): TGetOperationStatusResp; - GetOperationStatus(req: TGetOperationStatusReq, callback?: (error: void, response: TGetOperationStatusResp)=>void): void; - CancelOperation(req: TCancelOperationReq): TCancelOperationResp; - CancelOperation(req: TCancelOperationReq, callback?: (error: void, response: TCancelOperationResp)=>void): void; - CloseOperation(req: TCloseOperationReq): TCloseOperationResp; - CloseOperation(req: TCloseOperationReq, callback?: (error: void, response: TCloseOperationResp)=>void): void; - GetResultSetMetadata(req: TGetResultSetMetadataReq): TGetResultSetMetadataResp; - GetResultSetMetadata(req: TGetResultSetMetadataReq, callback?: (error: void, response: TGetResultSetMetadataResp)=>void): void; - FetchResults(req: TFetchResultsReq): TFetchResultsResp; - FetchResults(req: TFetchResultsReq, callback?: (error: void, response: TFetchResultsResp)=>void): void; - GetDelegationToken(req: TGetDelegationTokenReq): TGetDelegationTokenResp; - GetDelegationToken(req: TGetDelegationTokenReq, callback?: (error: void, response: TGetDelegationTokenResp)=>void): void; - CancelDelegationToken(req: TCancelDelegationTokenReq): TCancelDelegationTokenResp; - CancelDelegationToken(req: TCancelDelegationTokenReq, callback?: (error: void, response: TCancelDelegationTokenResp)=>void): void; - RenewDelegationToken(req: TRenewDelegationTokenReq): TRenewDelegationTokenResp; - RenewDelegationToken(req: TRenewDelegationTokenReq, callback?: (error: void, response: TRenewDelegationTokenResp)=>void): void; } diff --git a/tsconfig.json b/tsconfig.json index 43e7eae2..9da406df 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES6", + "target": "ES2018", "module": "commonjs", "declaration": true, "sourceMap": true,