Skip to content

Commit

Permalink
Break up tests & modules for easier testing.
Browse files Browse the repository at this point in the history
Deal with issue described in jestjs/jest#1075 (comment)

epm/packages/install has functions a, b, c which are independent but a can also call b and c

function a() {
  b();
  c();
}

The linked FB issue describes the cause and rationale (Jest works on "module" boundary) but TL;DR: it's easier if you split up your files

Some related links I found during this journey

 * https://medium.com/@qjli/how-to-mock-specific-module-function-in-jest-715e39a391f4
  * https://stackoverflow.com/questions/52650367/jestjs-how-to-test-function-being-called-inside-another-function
   * https://stackoverflow.com/questions/50854440/spying-on-an-imported-function-that-calls-another-function-in-jest/50855968#50855968
  • Loading branch information
John Schulz committed Sep 25, 2020
1 parent 037ba98 commit a76006f
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 140 deletions.
6 changes: 2 additions & 4 deletions x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,19 @@ import {
BulkUpgradePackagesFromRegistryRequestSchema,
} from '../../types';
import {
bulkInstallPackages,
getCategories,
getPackages,
getFile,
getPackageInfo,
handleInstallPackageFailure,
installPackage,
removeInstallation,
getLimitedPackages,
getInstallationObject,
} from '../../services/epm/packages';
import { defaultIngestErrorHandler } from '../../errors';
import { splitPkgKey } from '../../services/epm/registry';
import {
handleInstallPackageFailure,
bulkInstallPackages,
} from '../../services/epm/packages/install';

export const getCategoriesHandler: RequestHandler<
undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as Registry from '../registry';
import { getInstallationObject } from './index';
import {
BulkInstallPackagesParams,
BulkInstallResponse,
upgradePackage,
bulkInstallErrorToOptions,
} from './install';

export async function bulkInstallPackages({
savedObjectsClient,
packagesToUpgrade,
callCluster,
}: BulkInstallPackagesParams): Promise<BulkInstallResponse[]> {
const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) =>
Promise.all([
getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }),
Registry.fetchFindLatestPackage(pkgToUpgrade),
])
);
const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises);
const installResponsePromises = installedAndLatestResults.map(async (result, index) => {
const pkgToUpgrade = packagesToUpgrade[index];
if (result.status === 'fulfilled') {
const [installedPkg, latestPkg] = result.value;
return upgradePackage({
savedObjectsClient,
callCluster,
installedPkg,
latestPkg,
pkgToUpgrade,
});
} else {
return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason });
}
});
const installResults = await Promise.allSettled(installResponsePromises);
const installResponses = installResults.map((result, index) => {
const pkgToUpgrade = packagesToUpgrade[index];
if (result.status === 'fulfilled') {
return result.value;
} else {
return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason });
}
});

return installResponses;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObject } from 'src/core/server';
import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types';
import { getInstallType } from './install';

const mockInstallation: SavedObject<Installation> = {
id: 'test-pkg',
references: [],
type: 'epm-packages',
attributes: {
id: 'test-pkg',
installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
es_index_patterns: { pattern: 'pattern-name' },
name: 'test packagek',
version: '1.0.0',
install_status: 'installed',
install_version: '1.0.0',
install_started_at: new Date().toISOString(),
},
};
const mockInstallationUpdateFail: SavedObject<Installation> = {
id: 'test-pkg',
references: [],
type: 'epm-packages',
attributes: {
id: 'test-pkg',
installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
es_index_patterns: { pattern: 'pattern-name' },
name: 'test packagek',
version: '1.0.0',
install_status: 'installing',
install_version: '1.0.1',
install_started_at: new Date().toISOString(),
},
};

describe('getInstallType', () => {
it('should return correct type when installing and no other version is currently installed', () => {
const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined });
expect(installTypeInstall).toBe('install');

// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'update').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'reinstall').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'reupdate').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'rollback').toBe(false);
});

it('should return correct type when installing the same version', () => {
const installTypeReinstall = getInstallType({
pkgVersion: '1.0.0',
installedPkg: mockInstallation,
});
expect(installTypeReinstall).toBe('reinstall');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeReinstall === 'install').toBe(false);
});

it('should return correct type when moving from one version to another', () => {
const installTypeUpdate = getInstallType({
pkgVersion: '1.0.1',
installedPkg: mockInstallation,
});
expect(installTypeUpdate).toBe('update');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeUpdate === 'install').toBe(false);
});

it('should return correct type when update fails and trys again', () => {
const installTypeReupdate = getInstallType({
pkgVersion: '1.0.1',
installedPkg: mockInstallationUpdateFail,
});
expect(installTypeReupdate).toBe('reupdate');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeReupdate === 'install').toBe(false);
});

it('should return correct type when attempting to rollback from a failed update', () => {
const installTypeRollback = getInstallType({
pkgVersion: '1.0.0',
installedPkg: mockInstallationUpdateFail,
});
expect(installTypeRollback).toBe('rollback');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeRollback === 'install').toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
InstallationStatus,
KibanaAssetType,
} from '../../../types';

export { bulkInstallPackages } from './bulk_install_packages';
export {
getCategories,
getFile,
Expand All @@ -23,7 +25,7 @@ export {
SearchParams,
} from './get';

export { installPackage, ensureInstalledPackage } from './install';
export { handleInstallPackageFailure, installPackage, ensureInstalledPackage } from './install';
export { removeInstallation } from './remove';

type RequiredPackage = 'system' | 'endpoint';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,27 @@

import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types';
import { SavedObject } from 'src/core/server';
jest.mock('./install', () => ({
...(jest.requireActual('./install') as {}),
bulkInstallPackages: jest.fn(async () => {
return [
{
name: 'blah',
assets: [],
newVersion: '',
oldVersion: '',
statusCode: 200,
},
];
}),
}));

jest.mock('./install');
jest.mock('./bulk_install_packages');
jest.mock('./get', () => ({
...(jest.requireActual('./get') as {}),
getInstallation: jest.fn(async () => {
return mockInstallation.attributes;
}),
}));
import { getInstallType, ensureInstalledDefaultPackages } from './install';

import { bulkInstallPackages } from './bulk_install_packages';
const { ensureInstalledDefaultPackages } = jest.requireActual('./install');
import { savedObjectsClientMock } from 'src/core/server/mocks';
import { appContextService } from '../../app_context';
import { createAppContextStartContractMock } from '../../../mocks';

// if we add this assertion, TS will type check the return value
// and the editor will also know about .mockImplementation, .mock.calls, etc
const mockedBulkInstallPackages = bulkInstallPackages as jest.MockedFunction<
typeof bulkInstallPackages
>;

const mockInstallation: SavedObject<Installation> = {
id: 'test-pkg',
references: [],
Expand All @@ -49,22 +43,7 @@ const mockInstallation: SavedObject<Installation> = {
install_started_at: new Date().toISOString(),
},
};
const mockInstallationUpdateFail: SavedObject<Installation> = {
id: 'test-pkg',
references: [],
type: 'epm-packages',
attributes: {
id: 'test-pkg',
installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
es_index_patterns: { pattern: 'pattern-name' },
name: 'test packagek',
version: '1.0.0',
install_status: 'installing',
install_version: '1.0.1',
install_started_at: new Date().toISOString(),
},
};

describe('install', () => {
describe('ensureInstalledDefaultPackages', () => {
beforeEach(async () => {
Expand All @@ -74,68 +53,20 @@ describe('install', () => {
appContextService.stop();
});
it('should return an array of Installation objects when successful', async () => {
mockedBulkInstallPackages.mockImplementation(async function () {
return [
{
name: 'blah',
assets: [],
newVersion: '',
oldVersion: '',
statusCode: 200,
},
];
});
const soClient = savedObjectsClientMock.create();
const resp = await ensureInstalledDefaultPackages(soClient, jest.fn());
expect(resp).toEqual([mockInstallation.attributes]);
});
});
describe('getInstallType', () => {
it('should return correct type when installing and no other version is currently installed', () => {
const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined });
expect(installTypeInstall).toBe('install');

// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'update').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'reinstall').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'reupdate').toBe(false);
// @ts-expect-error can only be 'install' if no installedPkg given
expect(installTypeInstall === 'rollback').toBe(false);
});

it('should return correct type when installing the same version', () => {
const installTypeReinstall = getInstallType({
pkgVersion: '1.0.0',
installedPkg: mockInstallation,
});
expect(installTypeReinstall).toBe('reinstall');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeReinstall === 'install').toBe(false);
});

it('should return correct type when moving from one version to another', () => {
const installTypeUpdate = getInstallType({
pkgVersion: '1.0.1',
installedPkg: mockInstallation,
});
expect(installTypeUpdate).toBe('update');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeUpdate === 'install').toBe(false);
});

it('should return correct type when update fails and trys again', () => {
const installTypeReupdate = getInstallType({
pkgVersion: '1.0.1',
installedPkg: mockInstallationUpdateFail,
});
expect(installTypeReupdate).toBe('reupdate');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeReupdate === 'install').toBe(false);
});

it('should return correct type when attempting to rollback from a failed update', () => {
const installTypeRollback = getInstallType({
pkgVersion: '1.0.0',
installedPkg: mockInstallationUpdateFail,
});
expect(installTypeRollback).toBe('rollback');

// @ts-expect-error cannot be 'install' if given installedPkg
expect(installTypeRollback === 'install').toBe(false);
});
});
});
Loading

0 comments on commit a76006f

Please sign in to comment.