Skip to content

Commit

Permalink
Adds unit tests for server client
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonrhodes committed Oct 3, 2023
1 parent 7f4c183 commit 5c7b12c
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 17 deletions.
10 changes: 5 additions & 5 deletions x-pack/plugins/asset_manager/common/types_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
* 2.0.
*/

export interface GetHostsOptionsPublic {
export interface SharedAssetsOptionsPublic {
from: string;
to: string;
to?: string;
}

export interface GetServicesOptionsPublic {
from: string;
to: string;
export type GetHostsOptionsPublic = SharedAssetsOptionsPublic;

export interface GetServicesOptionsPublic extends SharedAssetsOptionsPublic {
parent?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function getHostsBySignals(
const { assets } = await collectHosts({
client: options.elasticsearchClient,
from: options.from,
to: options.to,
to: options.to || 'now',
sourceIndices: {
metrics: metricsIndices,
logs: options.sourceIndices.logs,
Expand Down
12 changes: 11 additions & 1 deletion x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
* 2.0.
*/

export type { GetHostsOptions, GetHostsOptionsInjected } from './shared_types';
import { validateESDate } from '../../validators/validate_es_date';
import type { GetHostsOptions, GetHostsOptionsInjected } from './shared_types';
export { getHostsByAssets } from './get_hosts_by_assets';
export { getHostsBySignals } from './get_hosts_by_signals';

export type { GetHostsOptions, GetHostsOptionsInjected };

export function validateGetHostsOptions(options: GetHostsOptions) {
validateESDate(options.from);
if (options.to) {
validateESDate(options.to);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function getServicesBySignals(
const { assets } = await collectServices({
client: options.elasticsearchClient,
from: options.from,
to: options.to,
to: options.to || 'now',
sourceIndices: {
apm: apmIndices,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
* 2.0.
*/

export type { GetServicesOptions, GetServicesOptionsInjected } from './shared_types';
import { validateESDate } from '../../validators/validate_es_date';
import type { GetServicesOptions, GetServicesOptionsInjected } from './shared_types';

export type { GetServicesOptions, GetServicesOptionsInjected };
export { getServicesByAssets } from './get_services_by_assets';
export { getServicesBySignals } from './get_services_by_signals';

export function validateGetServicesOptions(options: GetServicesOptions) {
validateESDate(options.from);
if (options.to) {
validateESDate(options.to);
}
}
148 changes: 148 additions & 0 deletions x-pack/plugins/asset_manager/server/lib/asset_client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
ElasticsearchClientMock,
elasticsearchClientMock,
} from '@kbn/core-elasticsearch-client-server-mocks';
import { AssetClient } from './asset_client';
import { MetricsDataClient, MetricsDataClientMock } from '@kbn/metrics-data-access-plugin/server';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';

function createAssetClient(source: 'signals' | 'assets', metricsDataClient: MetricsDataClient) {
return new AssetClient({
source,
sourceIndices: {
logs: 'my-logs*',
},
getApmIndices: jest.fn(),
metricsClient: metricsDataClient,
});
}

describe('Public assets client', () => {
let metricsDataClientMock: MetricsDataClient = MetricsDataClientMock.create();
let esClientMock: ElasticsearchClientMock =
elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
let soClientMock: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
// Reset mocks
esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
soClientMock = savedObjectsClientMock.create();
metricsDataClientMock = MetricsDataClientMock.create();

// ES returns no results, just enough structure to not blow up
esClientMock.search.mockResolvedValueOnce({
took: 1,
timed_out: false,
_shards: {
failed: 0,
successful: 1,
total: 1,
},
hits: {
hits: [],
},
});
});

describe('class instantiation', () => {
it('should successfully instantiate with source: signals', () => {
createAssetClient('signals', metricsDataClientMock);
});

it('should successfully instantiate with source: assets', () => {
createAssetClient('assets', metricsDataClientMock);
});
});

describe('getHosts', () => {
it('should query Elasticsearch via signals correctly', async () => {
const client = createAssetClient('signals', metricsDataClientMock);

await client.getHosts({
from: 'now-5d',
to: 'now-3d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
});

expect(metricsDataClientMock.getMetricIndices).toHaveBeenCalledTimes(1);
expect(metricsDataClientMock.getMetricIndices).toHaveBeenCalledWith({
savedObjectsClient: soClientMock,
});

const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();

expect(bool?.filter).toEqual([
{
range: {
'@timestamp': {
gte: 'now-5d',
lte: 'now-3d',
},
},
},
]);

expect(bool?.must).toEqual([
{
exists: {
field: 'host.hostname',
},
},
]);

expect(bool?.should).toEqual([
{ exists: { field: 'kubernetes.node.name' } },
{ exists: { field: 'kubernetes.pod.uid' } },
{ exists: { field: 'container.id' } },
]);
});

it('should query Elasticsearch via assets correctly', async () => {
const client = createAssetClient('assets', metricsDataClientMock);

await client.getHosts({
from: 'now-5d',
to: 'now-3d',
elasticsearchClient: esClientMock,
savedObjectsClient: soClientMock,
});

expect(metricsDataClientMock.getMetricIndices).not.toHaveBeenCalled();

const dsl = esClientMock.search.mock.lastCall?.[0] as SearchRequest | undefined;
const { bool } = dsl?.query || {};
expect(bool).toBeDefined();

expect(bool?.filter).toEqual([
{
range: {
'@timestamp': {
gte: 'now-5d',
lte: 'now-3d',
},
},
},
]);

expect(bool?.must).toEqual([
{
terms: {
'asset.kind': ['host'],
},
},
]);
});
});
});
10 changes: 9 additions & 1 deletion x-pack/plugins/asset_manager/server/lib/asset_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
*/

import { Asset } from '../../common/types_api';
import { getHostsByAssets, getHostsBySignals, GetHostsOptions } from './accessors/hosts';
import {
getHostsByAssets,
getHostsBySignals,
validateGetHostsOptions,
GetHostsOptions,
} from './accessors/hosts';
import {
getServicesByAssets,
getServicesBySignals,
GetServicesOptions,
validateGetServicesOptions,
} from './accessors/services';
import {
AssetClientClassOptions,
Expand All @@ -30,6 +36,7 @@ export class AssetClient {
}

async getHosts(options: GetHostsOptions): Promise<{ hosts: Asset[] }> {
validateGetHostsOptions(options);
const withInjected = this.injectOptions(options);
if (this.baseOptions.source === 'assets') {
return await getHostsByAssets(withInjected);
Expand All @@ -39,6 +46,7 @@ export class AssetClient {
}

async getServices(options: GetServicesOptions): Promise<{ services: Asset[] }> {
validateGetServicesOptions(options);
const withInjected = this.injectOptions(options);
if (this.baseOptions.source === 'assets') {
return await getServicesByAssets(withInjected);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import datemath from '@kbn/datemath';
import { AssetsValidationError } from './validation_error';

export function validateESDate(dateString: string) {
try {
datemath.parse(dateString);
} catch (error: any) {
throw new AssetsValidationError(`"${dateString}" is not a valid Elasticsearch date value`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

interface ErrorOptions {
statusCode?: number;
}

export class AssetsValidationError extends Error {
public statusCode: number;

constructor(message: string, { statusCode = 500 }: ErrorOptions = {}) {
super(message);
this.name = 'AssetsValidationError';
this.statusCode = statusCode;
}
}
16 changes: 12 additions & 4 deletions x-pack/plugins/asset_manager/server/routes/assets/hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* 2.0.
*/

import datemath from '@kbn/datemath';
import { createRouteValidationFunction } from '@kbn/io-ts-utils';
import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { GetHostAssetsQueryOptions, getHostAssetsQueryOptionsRT } from '../../../common/types_api';
import { debug } from '../../../common/debug_log';
import { SetupRouteOptions } from '../types';
import * as routePaths from '../../../common/constants_routes';
import { getClientsFromContext } from '../utils';
import { AssetsValidationError } from '../../lib/validators/validation_error';

export function hostsRoutes<T extends RequestHandlerContext>({
router,
Expand All @@ -27,20 +27,28 @@ export function hostsRoutes<T extends RequestHandlerContext>({
},
async (context, req, res) => {
const { from = 'now-24h', to = 'now' } = req.query || {};

const { elasticsearchClient, savedObjectsClient } = await getClientsFromContext(context);

try {
const response = await assetClient.getHosts({
from: datemath.parse(from)!.toISOString(),
to: datemath.parse(to)!.toISOString(),
from,
to,
elasticsearchClient,
savedObjectsClient,
});

return res.ok({ body: response });
} catch (error: unknown) {
debug('Error while looking up HOST asset records', error);

if (error instanceof AssetsValidationError) {
return res.customError({
statusCode: error.statusCode,
body: {
message: `Error while looking up host asset records - ${error.message}`,
},
});
}
return res.customError({
statusCode: 500,
body: { message: 'Error while looking up host asset records - ' + `${error}` },
Expand Down
16 changes: 13 additions & 3 deletions x-pack/plugins/asset_manager/server/routes/assets/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import datemath from '@kbn/datemath';
import { createRouteValidationFunction } from '@kbn/io-ts-utils';
import { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import {
Expand All @@ -16,6 +15,7 @@ import { debug } from '../../../common/debug_log';
import { SetupRouteOptions } from '../types';
import * as routePaths from '../../../common/constants_routes';
import { getClientsFromContext } from '../utils';
import { AssetsValidationError } from '../../lib/validators/validation_error';

export function servicesRoutes<T extends RequestHandlerContext>({
router,
Expand All @@ -34,8 +34,8 @@ export function servicesRoutes<T extends RequestHandlerContext>({
const { elasticsearchClient, savedObjectsClient } = await getClientsFromContext(context);
try {
const response = await assetClient.getServices({
from: datemath.parse(from)!.toISOString(),
to: datemath.parse(to)!.toISOString(),
from,
to,
parent,
elasticsearchClient,
savedObjectsClient,
Expand All @@ -44,6 +44,16 @@ export function servicesRoutes<T extends RequestHandlerContext>({
return res.ok({ body: response });
} catch (error: unknown) {
debug('Error while looking up SERVICE asset records', error);

if (error instanceof AssetsValidationError) {
return res.customError({
statusCode: error.statusCode,
body: {
message: `Error while looking up service asset records - ${error.message}`,
},
});
}

return res.customError({
statusCode: 500,
body: { message: 'Error while looking up service asset records - ' + `${error}` },
Expand Down
Loading

0 comments on commit 5c7b12c

Please sign in to comment.